Python super() and Multiple Inheritance

Features of using super().

Table of Contents

About super()


Docs:
super()
super(type, object_or_type=None)
Return a proxy object that delegates method calls to a parent or sibling class of type. This is useful for accessing inherited methods that have been overridden in a class.

A parent class is a class that has other classes derived from it. Sibling classes are classes that share a common parent.

# Child1 and Child2 are siblings

clas Parent:
    pass

class Child1(Parent):
    pass

class Child2(Parent):
    pass

In multiple inheritance, parent classes can also have a common parent class (the diamond problem).

# Parent1 and Parent2 are siblings

class A:
    pass

clas Parent1(A):
    pass

clas Parent2(A):
    pass

class Child(Parent1, Parent2):
    pass

Hierarchy of Related Classes With super()

The following example illustrates the use of super() in a class hierarchy.

All classes have a superclass. All subclasses of class A have common interface: "self.attr" and "self.method". Derived classes can specialize the interface, providing a specific implementation where necessary.

super() is useful for accessing inherited methods that have been overridden in a class.

In this case, we call parent.__init__ in each child class and parent.method in class D.

from abc import ABCMeta, abstractmethod

class A(metaclass=ABCMeta):
    def __init__(self, attr):
        print("A.__init__")
        self.attr = attr

    @abstractmethod
    def method(self):
        pass

class B(A):
    def __init__(self, attr):
        print("B.__init__")
        # super().__init__(attr) -> A.__init__(self, attr)
        super().__init__(attr)

    def method(self):
        print("B method")

class C(A):
    def __init__(self, attr):
        print("C.__init__")
        # super().__init__(attr) -> A.__init__(self, attr)
        super().__init__(attr)

   def method(self):
        print("C method")

class D(B):
    def __init__(self, attr):
        print("D.__init__")
        # super().__init__(attr) -> B.__init__(self, attr)
        super().__init__(attr)

    def method(self):
        # super().method() -> super(D, self).method()
        print("D method")
        super().method()

Creating instances.

b = B(1)
# B.__init__
# A.__init__
b.method()
# B method

c = C(2)
# C.__init__
# A.__init__
c.method()
# C method

d = D(3)
# D.__init__
# B.__init__
# A.__init__
d.method()
# D method
# B method

A derived class inherits all the methods, properties, and data attributes of the base class.

print(dir(d))
# [..., 'attr', 'method']

print(isinstance(d, A))
# True
print(isinstance(d, B))
# True

# type's method resolution order
print(D.mro())
# [<class '__main__.D'>, <class '__main__.B'>,
# <class '__main__.A'>, <class 'object'>]

Class D is also a subclass of A, but not directly.

print(D.__bases__)
# (<class '__main__.B'>,)

# list of immediate subclasses
print(A.__subclasses__())
# [<class '__main__.B'>, <class '__main__.C'>]
print(B.__subclasses__())
# [<class '__main__.D'>]

print(issubclass(D, A))
# True
print(issubclass(D, B))
# True

super() and Multiple Inheritance


Chain of Class.__init__ Calls

Python supports multiple inheritance. Multiple inheritance means that a class directly derives from multiple superclasses at the same time. These superclasses may not be related to each other.

In the following example, class B derives from two classes at the same time, and we want to use super(). This code works, but we can notice that A2.__init__ is not called. This can be a problem if we need to pass some argument to A1.__init__ and A2.__init__ or if we rely on some code being executed in A2.__init__.

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

    def method(self):
        print(self)

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

    def other_method(self):
        print(self)

class B(A1, A2):
    def __init__(self):
        super().__init__()

b = B()
# A1.__init__
print(dir(b))
# [..., 'method', 'other_method']
b.method()
# <__main__.B object at 0x00000213C0056E50>
b.other_method()
# <__main__.B object at 0x00000213C0056E50>

Here we encounter the first constraint: all classes in the hierarchy must use super(). To comply with this, we can add super().__init__() to classes A1 and A2. All Python3 classes implicitly have an object as their parent class, so we can call super().__init__() for A1 and A2.

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

    def method(self):
        print(self)

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

    def other_method(self):
        print(self)

class B(A1, A2):
    def __init__(self):
        super().__init__()

b = B()
# A1.__init__
# A2.__init__

But if we want to use some class from PSL or class from an external library in multiple inheritance, we cannot guarantee that all classes use super.


Passing Different Arguments

Another limitation arises if we need to pass some arguments to the __init__ methods. Each superclass may require a different number of values of attributes.

We can't just pass these values to super().__init__(<attrs list>). We will get a TypeError due to a mismatch of arguments with the __init__ parameters in superclasses.

class A1:
    def __init__(self, attr1):
        print("A1.__init__")
        super().__init__()
        self.attr1 = attr1

    def method(self):
        print(self)

class A2:
    def __init__(self, attr2, attr3):
        print("A2.__init__")
        super().__init__()
        self.attr2 = attr2
        self.attr3 = attr3

    def other_method(self):
        print(self)

class B(A1, A2):
    def __init__(self, attr1, attr2, attr3):
        super().__init__()
        # TypeError: A1.__init__() missing 1 required positional argument: 'attr1'

        # super().__init__(attr1)
        # TypeError: A2.__init__() missing 2 required positional arguments: 'attr2' and 'attr3'

        # super().__init__(attr1, attr2, attr3)
        # TypeError: A1.__init__() takes 2 positional arguments but 4 were given

b = B("attr1","attr2","attr3")

Solutions


Passing arguments to __init__: *args, **kwargs

To pass arguments through the __init__ call chain, we can use *args and **kwargs in class A1. Since there are no other classes after A2, we can leave it as is.

This option is suitable when you use your custom classes.

class A1:
    def __init__(self, attr1, *args, **kwargs):
        print("A1.__init__")
        super().__init__(*args, **kwargs)
        self.attr1 = attr1
        
   def method(self):
        print(self)

class A2:
    def __init__(self, attr2, attr3):
        print("A2.__init__")
        super().__init__()
        self.attr2 = attr2
        self.attr3 = attr3

    def other_method(self):
        print(self)

class B(A1, A2):
    def __init__(self, attr1, attr2, attr3):
        super().__init__(attr1, attr2, attr3)

b = B("attr1","attr2","attr3")
# A1.__init__
# A2.__init__
print(b.attr1, b.attr2, b.attr3)
# attr1 attr2 attr3

Calling __init__ of Each Superclass Separately

This option is suitable for both cases: when you use your custom classes, or when you use some external classes.

import tkinter as tk

class A:
    def __init__(self, attr):
        print("A.__init__")
        self.attr = attr

    def method(self):
        print(self)

class B(tk.Tk, A):
    def __init__(self, attr):
        tk.Tk.__init__(self)
        A.__init__(self, attr)

b = B("attr")
# A.__init__
print(b.attr)
# attr
print(b.master)
# None

Using Mixin Classes

Consider using mixins in multiple inheritance. A mixin is a class that contains only methods that provide additional functionality to other classes. Its methods are intended to be included or mixed with a child class.

A mixin does not define the structure and behavior of derived classes. It doesn't have any specific data attributes, so we don't need to call it's __init__. For simplicity, a mixin may not be derived from other classes.

Mixins are often used when you want to add the same features to many classes. These features are already implemented in the mixin.

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

    def method(self):
        print(self)

class SomeMixin:
    # class attributes or properties may exist
    attr = "attr"

    @property
    def some_property(self):
        print(self.__class__.__name__)

    def other_method(self):
        print(self)

class B(SomeMixin, A):
    def __init__(self, attr):
        super().__init__(attr)

b = B("attr")
# A.__init__
b.method()
# <__main__.B object at 0x00000213C0056E50>
b.other_method()
# <__main__.B object at 0x00000213C0056E50>
b.some_property
# B

References:

  1. Python Docs: super
  2. Docs: Method Resolution Order
  3. Python's super() considered super!
  4. Python's Super is nifty, but you can't use it
  5. Wiki: Mixin
  6. Wiki: Diamond problem