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)`.
Last updated: August 2026

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

  1. 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: A was visited before C. If class C had overridden a method originally defined in A, the outdated implementation in A would be found and executed first via the B -> A path, completely ignoring C's specialized override!
  2. 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.

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

  1. Extended Precedence (Subclass before Superclass): A subclass is always searched before any of its superclasses (e.g., $D$ always precedes $B, C, A$).
  2. 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$.
  3. 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:

L[C]=[C]+merge(L[B1],L[B2],,L[Bn],[B1,B2,,Bn])L[C] = [C] + \text{merge}(L[B_1], L[B_2], \dots, L[B_n], [B_1, B_2, \dots, B_n])

Where the merge operation works as follows:

  1. Look at the head (first element) of the first list in the merge collection.
  2. 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.
  3. Otherwise, look at the head of the next list in the collection and repeat the check.
  4. 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]$: 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}([B, A, \text{object}], [C, A, \text{object}], [B, C])

  1. Candidate $B$: Head of list 1. Is $B$ in the tail of list 2 ([A, object]) or list 3 ([C])? No. $\implies$ Extract $B$. L[D]=[D,B]+merge([A,object],[C,A,object],[C])L[D] = [D, B] + \text{merge}([A, \text{object}], [C, A, \text{object}], [C])
  2. 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$.
  3. Candidate $C$: Head of list 2. Is $C$ in the tail of any list? No. $\implies$ Extract $C$. L[D]=[D,B,C]+merge([A,object],[A,object])L[D] = [D, B, C] + \text{merge}([A, \text{object}], [A, \text{object}])
  4. Candidate $A$: Head of list 1. Not in tail of any list. $\implies$ Extract $A$. L[D]=[D,B,C,A]+merge([object],[object])L[D] = [D, B, C, A] + \text{merge}([\text{object}], [\text{object}])
  5. Candidate $\text{object}$: Extract $\text{object}$.

Final MRO: DBCAobject\text{Final MRO: } D \longrightarrow B \longrightarrow C \longrightarrow A \longrightarrow \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:

  1. Class.__mro__ Attribute: Returns an immutable tuple of class objects.
  2. Class.mro() Method: Returns a list of 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 instance d. To inspect the MRO from an instance, use type(d).__mro__.

Loading diagram...
Diamond Inheritance Graph and C3 Linearization Lookup Path

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.

Test Your Knowledge

Consider the following Python program implementing a diamond inheritance hierarchy:

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())
What is printed when this script executes?

A
B
C
D
Test Your Knowledge

Which of the following statements regarding the inspection of Method Resolution Order (MRO) in Python 3 is TRUE?

A
B
C
D
Test Your Knowledge

Consider the following cooperative multiple inheritance code:

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 is the exact output of this program?

A
B
C
D
Test Your Knowledge

What happens when Python evaluates the following class definition?

class Alpha: pass
class Beta(Alpha): pass
class Gamma(Alpha, Beta):
    pass

A
B
C
D