Multiple Inheritance & C3 Linearization

Master Python multiple inheritance: the diamond problem, the recursive C3 Linearization merge algorithm, MRO monotonicity invariants, and cooperative super().

03. Object-Oriented Systems, Descriptor Protocol & Metaprogramming8 min read

Python natively supports multiple inheritance: a class can inherit from two or more parent classes simultaneously.

However, multiple inheritance introduces serious theoretical challenges, most notably the Diamond Inheritance Problem, where multiple inheritance paths diverge and rejoin at a common base class.

To resolve method calls deterministically without duplicate executions or ambiguous dispatch, Python uses the C3 Linearization Algorithm to construct a single, ordered list known as the Method Resolution Order (MRO).

Linearization StandardAlgorithm
C3 Merge

Adopted in Python 2.3 to guarantee monotonicity and local precedence across complex hierarchies.

1. The Diamond Inheritance Dilemma#

Consider a classic diamond hierarchy:

Architecture Diagram
Rendering diagram...

If class D calls a method defined in A and overridden in B and C, which version should execute? In what order should cooperative initializers run?

Legacy Python 2.2 used a naive Depth-First Search (DFS) order (D -> B -> A -> C -> A -> object), which violated fundamental object-oriented principles: it evaluated grandparent A before sibling parent C!

2. The Formal C3 Linearization Algorithm#

Python resolves inheritance graphs using C3 Linearization. The algorithm defines the linearization L(C)L(C) of class CC inheriting from parent classes B1,B2,…,BnB_1, B_2, \dots, B_n through a recursive merge operation:

L(C)=C+merge(L(B1),L(B2),…,L(Bn),(B1,B2,…,Bn))L(C) = C + \text{merge}\left( L(B_1), L(B_2), \dots, L(B_n), (B_1, B_2, \dots, B_n) \right)
L(object)=(object)L(\text{object}) = (\text{object})

The Merge Selection Rule#

The merge operator evaluates the list candidates:

  1. Examine the head (first element) of the first list in the merge set.
  2. Check if this head appears in the tail (all elements after the first) of any other list in the merge set.
  3. If it does not appear in any tail:
  • Append that head class to the output MRO list.
  • Remove that class from every list in the merge set.
  1. If it appears in a tail:
  • Skip to the head of the next list in the merge set and repeat the check.
  1. If all heads appear in tails, C3 aborts, raising TypeError: Cannot create a consistent method resolution order (MRO).
Architecture Diagram
Rendering diagram...

Invariants Guaranteed by C3#

  1. Extended Child Precedence: Subclasses always precede parent classes (DD appears before BB and CC).
  2. Local Precedence Order: Parent order declared in class definition headers is preserved (BB appears before CC).
  3. Monotonicity: If class XX precedes class YY in L(C)L(C), then XX strictly precedes YY in the MRO of any subclass of CC.

3. Step-by-Step Mathematical Derivation#

Let us calculate the MRO for the diamond hierarchy where B(A)B(A), C(A)C(A), and D(B,C)D(B, C):

L(A)=A+merge(L(object),(object))=(A,object)L(A) = A + \text{merge}(L(\text{object}), (\text{object})) = (A, \text{object})
L(B)=B+merge(L(A),(A))=(B,A,object)L(B) = B + \text{merge}(L(A), (A)) = (B, A, \text{object})
L(C)=C+merge(L(A),(A))=(C,A,object)L(C) = C + \text{merge}(L(A), (A)) = (C, A, \text{object})

Now compute L(D)L(D):

L(D)=D+merge(L(B),L(C),(B,C))L(D) = D + \text{merge}(L(B), L(C), (B, C))
L(D)=D+merge((B,A,object),(C,A,object),(B,C))L(D) = D + \text{merge}\Big( (B, A, \text{object}), (C, A, \text{object}), (B, C) \Big)
  • Round 1: Head of list 1 is BB. Is BB in any tail?
  • Tails: (A, object), (A, object), (C). BB is not in any tail!
  • Pick BB. Output: (D, B).
  • Remaining: merge((A, object), (C, A, object), (C))
  • Round 2: Head of list 1 is AA. Is AA in any tail?
  • Tails: (object), (A, object)   ⟹  A\implies A is in the tail of list 2! Blocked!
  • Move to list 2 head: CC. Is CC in any tail?
  • Tails: (object), (A, object), ()   ⟹  C\implies C is not in any tail!
  • Pick CC. Output: (D, B, C).
  • Remaining: merge((A, object), (A, object))
  • Round 3: Head is AA. AA is not in any tail. Pick AA. Output: (D, B, C, A).
  • Round 4: Pick object.
L(D)=(D,B,C,A,object)\mathbf{L(D) = (D, B, C, A, \text{object})}

4. Demystifying super() in Cooperative Hierarchies#

A widespread misconception is that super() calls the immediate parent class. In Python, super() does not call the parent; it calls the NEXT class in the instance's MRO!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Base: def action(self): print("Base action") class Left(Base): def action(self): print("Left action start") super().action() print("Left action end") class Right(Base): def action(self): print("Right action start") super().action() print("Right action end") class Diamond(Left, Right): def action(self): print("Diamond action start") super().action() print("Diamond action end") d = Diamond() print(f"MRO Order: {[cls.__name__ for cls in Diamond.__mro__]}") print("\n=== Executing d.action() ===") d.action()

Notice that inside Left.action(), calling super().action() dispatched directly to Right.action(), even though Left does not inherit from Right! This cooperative dispatch allows every class in the diamond to execute exactly once.

5. Concept Verification#

Test your understanding of multiple inheritance and C3 linearization:

Concept check

What does super().method() actually invoke in Python?

Concept check

Which invariant is guaranteed by the C3 Linearization algorithm to ensure inheritance hierarchies remain predictable?