6.2 Multiple Inheritance and Method Resolution Order (MRO)
Key Takeaways
- Multiple inheritance allows a Python class to derive from multiple base classes simultaneously (`class Child(Base1, Base2):`), introducing the classic Diamond Problem when classes share common ancestors.
- Python resolves all inheritance hierarchies using the C3 Linearization Algorithm, which guarantees monotonicity and preserves local base class declaration order.
- The Method Resolution Order (MRO) is an unambiguous linear sequence inspected via `Class.__mro__` (tuple) or `Class.mro()` (list), always ending with `object`.
- In cooperative multiple inheritance, `super()` does not call the syntactic parent class; it calls the next class in the runtime MRO sequence.
- If a proposed inheritance hierarchy contains circular dependencies or contradictory orderings, Python rejects class definition with a `TypeError: Cannot create a consistent method resolution order (MRO)`.
Multiple Inheritance and Method Resolution Order (MRO)
While single inheritance allows a class to specialize the behavior of a single parent, real-world domain modeling often demands combining capabilities from multiple independent sources. Python supports multiple inheritance, enabling a subclass to inherit attributes and methods from two or more base classes simultaneously.
However, multiple inheritance introduces architectural complexities—most notably the Diamond Problem and potential attribute collision. To eliminate ambiguity, Python employs a deterministic, mathematically rigorous algorithm known as C3 Linearization to construct an unambiguous lookup path called the Method Resolution Order (MRO).
1. Multiple Inheritance Syntax and Lookup Basics
A class declares multiple inheritance by listing its base classes in comma-separated order inside the class definition header:
class BaseA:
def action(self):
return "Action from BaseA"
class BaseB:
def action(self):
return "Action from BaseB"
def helper(self):
return "Helper from BaseB"
class Child(BaseA, BaseB):
pass
c = Child()
print(c.action()) # Output: Action from BaseA (BaseA listed before BaseB)
print(c.helper()) # Output: Helper from BaseB (Found in BaseB)
Left-to-Right Priority
In simple multiple inheritance scenarios without shared ancestors, Python resolves attribute lookups using a left-to-right priority based on the order classes are listed in the inheritance header. In class Child(BaseA, BaseB), BaseA is searched before BaseB.
2. The Diamond Problem
The Diamond Problem (also called the inheritance diamond or deadly diamond of death) occurs when two classes B and C inherit from a common ancestor A, and a derived class D inherits from both B and C.
A
/ \
B C
\ /
D
Why Naive Search Algorithms Fail
- Naive Depth-First Search (DFS):
- In legacy Python (Python 2.2 and earlier old-style classes), naive DFS traversed
D -> B -> A -> C -> A. - The Flaw:
Awas visited beforeC. If classChad overridden a method originally defined inA, the outdated implementation inAwould be found and executed first via theB -> Apath, completely ignoringC's specialized override!
- In legacy Python (Python 2.2 and earlier old-style classes), naive DFS traversed
- Naive Breadth-First Search (BFS):
- Traverses
D -> B -> C -> A. - The Flaw: BFS breaks local precedence in non-symmetric hierarchies where intermediate subclasses have deeper specialized chains.
- Traverses
To resolve this once and for all, Python 2.3 introduced the C3 Linearization Algorithm for all new-style classes (the standard and only model in Python 3).
3. The C3 Linearization Algorithm
C3 Linearization computes a deterministic, monotonic ordering of classes. For any class $C$, its linearization is denoted as $L[C]$, representing the ordered sequence of classes searched when resolving methods and attributes.
The Three Mathematical Guarantees of C3
- Extended Precedence (Subclass before Superclass): A subclass is always searched before any of its superclasses (e.g., $D$ always precedes $B, C, A$).
- Local Precedence Order (Declaration Order Preserved): Direct base classes are searched in the exact left-to-right order declared in the class header. In
class D(B, C):, $B$ is always searched before $C$. - Monotonicity: If class $X$ precedes class $Y$ in the linearization of any parent class $P$, then $X$ will precede $Y$ in the linearization of any subclass derived from $P$. A derived class cannot flip or reverse the relative search order of its ancestors.
How C3 Calculates Linearization
The linearization of a class $C$ with direct base classes $B_1, B_2, \dots, B_n$ is defined by the recurrence formula:
Where the merge operation works as follows:
- Look at the head (first element) of the first list in the merge collection.
- If this head is not present in the tail (all elements except the first) of any other list in the merge collection, add it to the output linearization and remove it from all lists in the collection.
- Otherwise, look at the head of the next list in the collection and repeat the check.
- Repeat until all lists are empty (successful linearization) or no valid candidate can be extracted (inconsistent MRO error).
Resolving the Classic Diamond with C3
Given:
class A(object)$\implies L[A] = [A, \text{object}]$class B(A)$\implies L[B] = [B, A, \text{object}]$class C(A)$\implies L[C] = [C, A, \text{object}]$class D(B, C)
Calculation for $L[D]$:
- Candidate $B$: Head of list 1. Is $B$ in the tail of list 2 (
[A, object]) or list 3 ([C])? No. $\implies$ Extract $B$. - Candidate $A$: Head of list 1. Is $A$ in the tail of list 2 (
[A, object])? Yes ($A$ is in the tail after $C$!). Skip $A$. - Candidate $C$: Head of list 2. Is $C$ in the tail of any list? No. $\implies$ Extract $C$.
- Candidate $A$: Head of list 1. Not in tail of any list. $\implies$ Extract $A$.
- Candidate $\text{object}$: Extract $\text{object}$.
Notice how $C$ is visited before $A$, ensuring that $C$'s overrides take precedence over $A$'s default implementation!
4. Inspecting the MRO: __mro__ and mro()
Python provides two standard mechanisms to inspect a class's computed MRO at runtime:
Class.__mro__Attribute: Returns an immutabletupleof class objects.Class.mro()Method: Returns alistof class objects.
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(D.__mro__)
# Output: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
print(D.mro())
# Output: [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
[!NOTE]
__mro__and.mro()are attributes of the class object (e.g.,D.__mro__), not the instanced. To inspect the MRO from an instance, usetype(d).__mro__.
5. Cooperative Multiple Inheritance with super()
The most profound aspect of Python's super() in multiple inheritance is that super() does not call the syntactic parent class; it calls the NEXT class in the instance's MRO chain.
When multiple classes in a hierarchy participate cooperatively by each invoking super().method(), method calls propagate predictably through every class in the MRO without duplicate invocations or missed steps:
class Root:
def process(self):
print("Root.process executed")
class StageA(Root):
def process(self):
print("StageA: start")
super().process() # In Diamond, super() delegates to StageB, NOT Root!
print("StageA: end")
class StageB(Root):
def process(self):
print("StageB: start")
super().process() # Delegates to Root
print("StageB: end")
class Pipeline(StageA, StageB):
def process(self):
print("Pipeline: start")
super().process() # Delegates to StageA
print("Pipeline: end")
p = Pipeline()
p.process()
Output:
Pipeline: start
StageA: start
StageB: start
Root.process executed
StageB: end
StageA: end
Pipeline: end
Notice how StageA's call to super().process() invokes StageB.process(). Even though StageB is not a parent of StageA, StageB follows StageA in Pipeline's MRO (Pipeline -> StageA -> StageB -> Root -> object).
6. Inconsistent MRO and Class Creation Failures
If an inheritance hierarchy violates the C3 linearization constraints (such as creating a circular dependency or reversing the order of base classes), Python detects the contradiction at class definition time and refuses to create the class, raising a TypeError.
Example of an Inconsistent MRO
class X: pass
class Y: pass
# A inherits from X then Y (X precedes Y)
class A(X, Y): pass
# B inherits from Y then X (Y precedes X - contradicting A!)
class B(Y, X): pass
# Attempting to inherit from both A and B
try:
class Conflicted(A, B):
pass
except TypeError as err:
print("Class creation failed:", err)
# Output: Class creation failed: Cannot create a consistent method resolution
# order (MRO) for bases X, Y
Because A requires X before Y, but B requires Y before X, C3 cannot establish a valid order that satisfies both constraints without violating monotonicity. Python immediately halts with TypeError.
Consider the following Python program implementing a diamond inheritance hierarchy:
What is printed when this script executes?class A:
def identify(self):
return "A"
class B(A):
def identify(self):
return "B"
class C(A):
def identify(self):
return "C"
class D(B, C):
pass
class E(C, B):
pass
print(D().identify() + E().identify())
Which of the following statements regarding the inspection of Method Resolution Order (MRO) in Python 3 is TRUE?
Consider the following cooperative multiple inheritance code:
What is the exact output of this program?class Base:
def step(self):
return ["Base"]
class Left(Base):
def step(self):
return ["Left"] + super().step()
class Right(Base):
def step(self):
return ["Right"] + super().step()
class Combined(Left, Right):
def step(self):
return ["Combined"] + super().step()
print(" -> ".join(Combined().step()))
What happens when Python evaluates the following class definition?
class Alpha: pass
class Beta(Alpha): pass
class Gamma(Alpha, Beta):
pass