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:
- Local Precedence Order: Children precede their parents, and base classes listed in a class definition maintain their left-to-right order.
- 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:
Bottom.process()executes, prints"Bottom executed", and callssuper().process().- Python inspects the instance's MRO. The class following
BottomisLeft. Left.process()executes, prints"Left executed", and callssuper().process().- Even though
Left's syntactic parent isRoot, the next class in the runtime instance's MRO isRight. Control shifts horizontally toRight.process(). Right.process()executes, prints"Right executed", and callssuper().process().- 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:
- Uniform Method Signatures: Because a class cannot
predict which sibling or ancestor will follow it in a subclass's MRO,
cooperative methods should accept
*argsand**kwargsand pass them along tosuper():def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - Universal Participation: If any class in the
inheritance chain omits
super(), the chain breaks prematurely, preventing subsequent classes in the MRO from executing. - Terminal Base Class: The final class in the
cooperative chain (before
object) must safely consume or terminate any remaining arguments to avoid passing unhandled parameters toobject.__init__.