This advertisement helps me to keep writing articles on this website

Python multiple inheritance. Not all base initializers are executed.

Python supports multiple inheritance. When all base classes have initializers, how do you make sure each one is called by your inherited class?

The problem. Not all base initializers are executed.

If you are reading this, you are probably using Python multiple inheritance for mixins or any other reason. You also might have wondered why not all your base initializers are called, even if you use super().__init__().

To explain why this is happening, first look at this single inheritance example:

class A:
    def __init__(self):
        print("A.__init__")

class C(A):
    def __init__(self):
        super().__init__()
        print("C.__init__")

C()

Output:

A.__init__
C.__init__
  1. Class C is instantiated.
  2. C.__init__ is executed and first calls super().__init__().
  3. Class A prints A.__init__.
  4. Class C prints C.__init__.

Show Method Resolution Order

At the end of the code, a print statement is added to show the Method Resulution Order:

print(C.mro())

Output:

[<class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

mro() prints the inheritance tree, where you can see that class C is inherited from class A and class A is inherited from object.

Multiple inheritance

Class B is added to the code and class C now inherits from both A and B:

class A:
    def __init__(self):
        print("A.__init__")

class B:
    def __init__(self):
        print("B.__init__")

class C(A, B):
    def __init__(self):
        super().__init__()
        print("C.__init__")

C()
print(C.mro())

Output:

A.__init__
C.__init__
[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]

In the output you see that the MRO goes from C -> A -> B -> object. If you want to know what algorithm is used here, I refer you to the Python docs.

For our example, it suffices to say that the inheritance goes from top to bottom, left to right. What is important to know, it that instead of an inheritance tree, we ended up with an inheritance graph:

      object
      /    \   
class A    class B
      \    /
      class C

The question now is:

If class C calls super…who is super? Who is super in a dependency graph?

I have programmed 20 years in C#. C# is a single inheritance language and am used to thinking super being the parent class. But in Python, where multiple inheritance is supported, super can be parent or sibling

Who is super?

We know that super is a reference to the parent (or sibling class) but when the type of super() is printed, it shows type: <class 'super'>

class C(A, B):
    def __init__(self):
        print(type(super()))

Output:

<class 'super'>

According to the documentation, super() returns a proxy object that delegates method calls to a parent or sibling class. The proxy object does not tell me what the parent or sibling is, but we can still look at the result of the mro function.

[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]

We can determine that:

  1. super() of C is A
  2. super() of A is B
  3. super() of B is object

graph

This means that if the initializer in class A would call super().__init__, the init method of class B should execute. In the following example you can see that is the case:

class A:
    def __init__(self):
        super().__init__()
        print("A.__init__")

class B:
    def __init__(self):
        print("B.__init__")

class C(A, B):
    def __init__(self):
        super().__init__()
        print("C.__init__")

C()

Output:

B.__init__
A.__init__
C.__init__

Cooperative calls

To make sure that all the classes in the graph call their parent or sibling initializers, you need to call super().__init__() in each class initializer. Pythons MRO created the correct graph and super().__init__() calls the appropriate parent or sibling initializer. This technique is also known as cooperative inheritance or call-next-method.

You might have noticed in the last example that class B does not call super().__init__(). In this example that is not a problem since B’s parent is object. But this code would cause problems, if you decide to inherit B from something else.

This brings me to an interesting point. Let’s say you use class X from a 3d party library and you don’t know if X calls super().__init__(), you can put them as the last argument in the class definition like this:

class A:
    def __init__(self):
        super().__init__()
        print("A.__init__")

class X:
    def __init__(self):
        print("X.__init__")

class C(A, X):  # X as last argument
.
.
.

C will call A.__init__ and A will call X.__init__. As long as you don’t care about any initializers above A and X, you won’t have a problem.

Conclusion

To allow cooperative calls in Python, where the initializer of every class in the graph is called, use super().__init__() in each init function.

Here is a complete example where all classes call super().__init__():

class A:
    def __init__(self):
        super().__init__()
        print("A.__init__")

class B:
    def __init__(self):
        super().__init__()
        print("B.__init__")

class C(A, B):
    def __init__(self):
        super().__init__()
        print("C.__init__")

C()

Output:

B.__init__
A.__init__
C.__init__
Written by Loek van den Ouweland on August 13, 2020. Questions regarding this artice? You can send them to the address below.
By using this site, you acknowledge that you have read and understand our Cookie and Privacy Policy.