Understanding Python super() in Multiple Inheritance

In Python, the super() built-in function does not simply refer to an immediate base class; instead, it dynamically delegates method calls to the next class along an instance's Method Resolution Order (MRO). In cooperative multiple inheritance, super() enables sibling and ancestor classes to work together by forming a deterministic chain of execution, ensuring each class in complex inheritance hierarchies—such as the diamond pattern—is visited exactly once.

The Role of Method Resolution Order (MRO)

To resolve method calls in multiple inheritance, Python computes a linear execution order known as the Method Resolution Order (MRO) using the C3 Linearization algorithm. The algorithm guarantees two properties:

  1. Local Precedence Order: Children precede their parents, and base classes listed in a class definition maintain their left-to-right order.
  2. Monotonicity: If a class precedes another in one class's MRO, it must precede it in any subclass's MRO.

When super().method() is called, Python looks at the MRO of the original caller instance (the runtime type of self), not the class where the super() call is written. super() finds the current class in that MRO list and invokes the method on the subsequent class in the chain.

You can inspect an object's MRO using the __mro__ attribute or the mro() method:

class Base:
    pass

class A(Base):
    pass

class B(Base):
    pass

class Child(A, B):
    pass

print(Child.mro())
# Output: [<class '__main__.Child'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <class 'object'>]

How Cooperative Calling Works

Cooperative multiple inheritance relies on every class in the hierarchy invoking super(). When a method executes, it passes control to the next class in the MRO until it reaches the root class (ultimately object).

Consider the diamond inheritance pattern:

class Root:
    def process(self):
        print("Root executed")

class Left(Root):
    def process(self):
        print("Left executed")
        super().process()

class Right(Root):
    def process(self):
        print("Right executed")
        super().process()

class Bottom(Left, Right):
    def process(self):
        print("Bottom executed")
        super().process()

When invoking Bottom().process(), the MRO is: Bottom \(\rightarrow\) Left \(\rightarrow\) Right \(\rightarrow\) Root \(\rightarrow\) object.

Execution proceeds as follows:

  1. Bottom.process() executes, prints "Bottom executed", and calls super().process().
  2. Python inspects the instance's MRO. The class following Bottom is Left.
  3. Left.process() executes, prints "Left executed", and calls super().process().
  4. Even though Left's syntactic parent is Root, the next class in the runtime instance's MRO is Right. Control shifts horizontally to Right.process().
  5. Right.process() executes, prints "Right executed", and calls super().process().
  6. Control moves to Root.process(), which prints "Root executed".

Output:

Bottom executed
Left executed
Right executed
Root executed

Without super(), manually calling direct base classes (e.g., Left.process(self) and Right.process(self)) would cause Root.process() to execute twice, leading to duplicate operations.

Requirements for Reliable Cooperative Inheritance

For cooperative inheritance to succeed without runtime errors, classes must follow specific design practices: