The Secret Life of Python: super() and the Method Resolution Order
Source: Dev.to
Why super() doesn’t mean “parent class” (and what it really does)
Timothy stared at his screen in complete bewilderment. He’d written what seemed like straightforward inheritance code, but the output made absolutely no sense.
class A:
def process(self):
print("A.process()")
class B(A):
def process(self):
print("B.process()")
super().process()
class C(A):
def process(self):
print("C.process()")
super().process()
class D(B, C):
def process(self):
print("D.process()")
super().process()
d = D()
d.process()
He expected the output to be:
D.process()
B.process()
A.process()
After all, D inherits from B, and B inherits from A. That’s how inheritance works, right?
But when he ran the code, he got:
D.process()
B.process()
C.process()
A.process()
The Method Resolution Order (MRO)
“
super()doesn’t call your parent class, Timothy. It calls the next class in the Method Resolution Order,” Margaret explained.
Every class in Python has an attribute called __mro__—the Method Resolution Order. It’s a tuple that defines the exact sequence Python follows when looking up attributes.
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
print(D.__mro__)
Output
(, , ,
, )
This tuple is the order Python searches for methods and attributes:
DBCAobject
super() means “the next class in the MRO,” not “my parent class.”
The MRO also affects isinstance() and issubclass():
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
obj = D()
print(isinstance(obj, D)) # True
print(isinstance(obj, B)) # True
print(isinstance(obj, C)) # True
print(isinstance(obj, A)) # True
print(isinstance(obj, object)) # True
print(issubclass(D, B)) # True
print(issubclass(D, C)) # True
print(issubclass(D, A)) # True
print(D.__mro__) # All the classes where isinstance returns True
Tracing the Original Example
class A:
def process(self):
print("A.process()")
class B(A):
def process(self):
print("B.process()")
super().process() # Next in MRO after B
class C(A):
def process(self):
print("C.process()")
super().process() # Next in MRO after C
class D(B, C):
def process(self):
print("D.process()")
super().process() # Next in MRO after D
print(D.__mro__) # (, , , , )
d = D()
d.process()
Execution flow
D.process()→ prints “D.process()”,super()→B.process()B.process()→ prints “B.process()”,super()→C.process()C.process()→ prints “C.process()”,super()→A.process()A.process()→ prints “A.process()”
The key insight is that super() in B doesn’t look at B’s parent (A); it looks at the next class in the MRO of the instance (D), which is C.
Different Instances, Different Paths
# Instance of B directly
b = B()
print("MRO of B:", B.__mro__) # (, , )
b.process()
# Output:
# B.process()
# A.process()
# Instance of D
d = D()
print("\nMRO of D:", D.__mro__) # (, , , , )
d.process()
# Output:
# D.process()
# B.process()
# C.process()
# A.process()
When you call b.process(), the MRO is B → A → object, so super() in B calls A. When you call d.process(), the MRO is D → B → C → A → object, so super() in B calls C.
How Python Builds the MRO: C3 Linearization
Python determines the MRO using C3 linearization (also called C3 superclass linearization). The algorithm follows three rules:
- Children before parents (local precedence order)
- Left‑to‑right ordering (from the inheritance list)
- Monotonicity (preserve order from parent MROs)
Monotonicity ensures that if class A appears before class B in any parent’s MRO, then A must appear before B in the child’s MRO (unless explicitly overridden).
Example: Building the MRO for D(B, C)
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
The C3 algorithm works as follows:
-
Start with the class itself
L(D) = D + merge(L(B), L(C), [B, C]) -
Expand parent linearizations
L(B) = B, A, object L(C) = C, A, object L(D) = D + merge([B, A, object], [C, A, object], [B, C]) -
Merge (take the first head that doesn’t appear in any other tail)
merge([B, A, object], [C, A, object], [B, C]): heads → B, C, B B is not in any tail → take B D, B + merge([A, object], [C, A, object], [C]): heads → A, C, C A is in a tail → skip C is not in any tail → take C D, B, C + merge([A, object], [A, object]): heads → A, A A is not in any tail → take A D, B, C, A + merge([object], [object]): heads → object, object object is not in any tail → take object Final MRO: D, B, C, A, object
Thus, Python computes the MRO (, , , , ), which explains why super() in B calls C when the instance is of type D.