Search code examples
pythonpython-3.xclassinheritanceabstract-class

Prohibit addition of new methods to a Python child class


I have two classes that are supposed to implement the same test cases for two independent libraries (let's call them LibA and LibB). So far I define the test methods to be implemented in an abstract base class which ensures that both test classes implement all desired tests:

from abc import ABC, abstractmethod

class MyTests(ABC):
    @abstractmethod
    def test_foo(self):
        pass

class TestsA(MyTests):
    def test_foo(self):
        pass

class TestsB(MyTests):
    def test_foo(self):
        pass

This works as expected, but what may still happen is that someone working on LibB accidentally adds a test_bar() method to TestB instead of the base class. The missing test_bar() in the TestA class would go unnoticed in that case.

Is there a way to prohibit the addition of new methods to an (abstract) base class? The objective is to force the addition of new methods to happen in the base class and thus force the implementation of new methods in all derived classes.


Solution

  • Yes. It can be done through a metaclass, or from Python 3.6 onwards, with a check in __init_subclass__ of the baseclass.

    __init_sublass__ is a special method called by the language each time a subclass is instantiated. So it can check if the new class have any method that is not present in any of the superclasses and raise a TypeError when the subclass is declared. (__init_subclass__ is converted to a classmethod automatically)

    class Base(ABC):
        ...
        def __init_subclass__(cls, *args, **kw):
            super().__init_subclass__(*args, **kw)
            # By inspecting `cls.__dict__` we pick all methods declared directly on the class
            for name, attr in cls.__dict__.items():
                attr = getattr(cls, name)
                if not callable(attr):
                    continue
                for superclass in cls.__mro__[1:]:
                    if name in dir(superclass):
                        break
                else:
                    # method not found in superclasses:
                    raise TypeError(f"Method {name} defined in {cls.__name__}  does not exist in superclasses")
    
    
    

    Note that unlike the TypeError raised by non-implemented abstractmethods, this error is raised at class declaration time, not class instantiation time. If the later is desired, you have to use a metaclass and move the check to its __call__ method - however that complicates things, as if one method is created in an intermediate class, that was never instantiated, it won't raise when the method is available in the leaf subclass. I guess what you need is more along the code above.