backend / python core / tricky questions / 10_super_mro_diamond.md

super() and MRO with multiple inheritance

3 min read source

super() and MRO with multiple inheritance

The gotcha

In multiple inheritance, super().method() doesn’t necessarily call the parent class you wrote next to the class — it calls the next class in the MRO (Method Resolution Order), which is computed by C3 linearization across all bases.

Minimal repro

class A:
    def hello(self):
        print("A")

class B(A):
    def hello(self):
        print("B")
        super().hello()

class C(A):
    def hello(self):
        print("C")
        super().hello()

class D(B, C):
    def hello(self):
        print("D")
        super().hello()

D().hello()
# D
# B
# C    ← surprise: B's super() doesn't go to A directly, it goes to C
# A

print(D.__mro__)
# (D, B, C, A, object)

Why it happens

super() returns a proxy that consults the MRO of type(self), not the static parent of the current class. C3 linearization merges all parent MROs into a consistent total order: D → B → C → A → object. When B.hello calls super(), the next class for this instance is C, not A.

This is exactly what cooperative multiple inheritance needs — every class hands off to the next one without knowing the others. But it confuses people coming from single-inheritance languages.

Walking through C3 linearization

C3 (named after the three properties it preserves: Consistency, Child precedence, Consistent overall ordering) computes the MRO via a deterministic merge algorithm.

For class C(B1, B2, ..., Bn):

L[C] = C + merge(L[B1], L[B2], ..., L[Bn], [B1, B2, ..., Bn])

Where L[X] is the MRO of X. The merge step takes a list of lists and emits one class at a time:

Pick the first head of the first non-empty list that is not in the tail of any other list. Remove it from all lists. Repeat. If no head qualifies, raise TypeError: cannot create a consistent MRO.

Worked example for the diamond above

class A: ...
class B(A): ...
class C(A): ...
class D(B, C): ...

Step 1 — base MROs:

L[A]      = [A, object]
L[B]      = [B, A, object]
L[C]      = [C, A, object]

Step 2 — L[D] = D + merge(L[B], L[C], [B, C]):

merge( [B, A, object], [C, A, object], [B, C] )
Iteration Lists Pick Why
1 [B,A,object], [C,A,object], [B,C] B head of list 1; B is not in any tail → take it
2 [A,object], [C,A,object], [C] C head of list 1 is A, but A appears in tail of list 2 (after C) → reject. Try head of list 2: C is not in any tail → take it
3 [A,object], [A,object], [] A A is head of both, not in any tail → take it
4 [object], [object], [] object head, no conflicts → take it

Result:

L[D] = [D, B, C, A, object]    ← matches D.__mro__

When C3 fails

class X(A, B): ...
class Y(B, A): ...
class Z(X, Y): ...
# TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B

X says A before B. Y says B before A. There’s no total order satisfying both, so C3’s “head not in any tail” condition can never be met. The runtime catches this at class-creation time, not at runtime.

Why C3 over simpler algorithms

Earlier Python (pre-2.3) used a depth-first left-to-right walk, which violated monotonicityD’s MRO didn’t have to respect the orders in B and C. C3 was adopted from Dylan to fix three guarantees:

  1. Local precedence: a class’s bases appear in the MRO in declaration order.
  2. Monotonicity: if X precedes Y in some parent’s MRO, it precedes Y in every descendant’s MRO too.
  3. Consistency: the MRO is unique and total (no cycles, every class appears once).

The “head not in any tail” rule is what enforces monotonicity.

How to avoid

For cooperative multiple inheritance, every method in the chain must:

  1. Accept **kwargs and pass them along
  2. Call super().method(...)
  3. Have a base class (often object implicitly) that doesn’t propagate further

Always inspect Cls.__mro__ if you’re confused. Use mro() for the same info dynamically.

If you don’t need cooperative MI, avoid multiple inheritance entirely. Use mixins sparingly and document the linearization, or use composition.

Interview angle

Whiteboard a diamond and ask the candidate to predict the print order. “If D.__mro__ is (D, B, C, A, object), what does D().hello() print, and why does B.hello’s super() go to C?”