Study guide

6.2 Multiple Inheritance and Method Resolution Order (MRO)

OpenEDG PCAP — Certified Associate in Python Programming Exam

Track your progress as you read

Finished sections get a check mark.

1 Chapter 1: The PCAP-31-03 Exam Blueprint and Test-Day Logistics

1.1 The PCAP-31-03 Blueprint, Scoring, and Exam-Day Logistics

2 Chapter 2: Modules, Packages, and the Standard Library

2.1 Module Imports and Namespace Mechanics 2.2 Module Construction and Execution 2.3 Package Hierarchy and Navigation 2.4 Standard Library Modules and Pip

3 Chapter 3: Exceptions and Error Handling

3.1 Exception Control Flow and Syntax 3.2 The Built-in Exception Hierarchy 3.3 Raising Exceptions and Assertions 3.4 Custom Exceptions and Exception Chaining

4 Chapter 4: Strings, Encodings, and Text Processing

4.1 Character Encodings and String Immutability 4.2 String Indexing, Slicing, and Comparison 4.3 String Inspection and Classification Methods 4.4 String Transformation and Formatting Methods 4.5 Iterating, Sorting, and Comparing Strings Against Other Types

5 Chapter 5: Object-Oriented Programming Fundamentals

5.1 Classes, Instances, and Attributes 5.2 Encapsulation, Privacy, and Name Mangling 5.3 Dunder Methods and Object Lifecycle 5.4 Discovering Class Structure: __name__, __module__, __bases__, and Identity

6 Chapter 6: Advanced OOP, Inheritance, and Metaprogramming

6.1 Single Inheritance, Method Overriding, and Polymorphism 6.2 Multiple Inheritance and Method Resolution Order (MRO) 6.3 Class Methods, Static Methods, and Properties

7 Chapter 7: Functional Python, Comprehensions, and Closures

7.1 List, Dictionary, and Set Comprehensions 7.2 Lambda Expressions and Higher-Order Functions 7.3 Variable Scopes, the LEGB Rule, and Closures

8 Chapter 8: Iterators, Generators, and File I/O

8.1 Iterators and the Iteration Protocol 8.2 Generator Functions and Generator Expressions 8.3 File Input/Output and Context Managers 8.4 Predefined Streams, Handles vs. Streams, and errno Diagnostics

6.2 Multiple Inheritance and Method Resolution Order (MRO)

Key Takeaways Explain This Topic AI

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

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!

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 C C , its linearization is denoted as L [ C ] L[C] 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 D D always precedes B , C , A B, C, A 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 B B is always searched before C C C .

Monotonicity: If class X X X precedes class Y Y Y in the linearization of any parent class P P P , then X X X will precede Y Y Y in the linearization of any subclass derived from P P 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 C C with direct base classes B 1 , B 2 , … , B n B_1, B_2, \dots, B_n B 1 ​ , B 2 ​ , … , B n ​ is defined by the recurrence formula: L [ C ] = [ C ] + merge ( L [ B 1 ] , L [ B 2 ] , … , L [ B n ] , [ B 1 , B 2 , … , B n ] ) L[C] = [C] + \text{merge}(L[B_1], L[B_2], \dots, L[B_n], [B_1, B_2, \dots, B_n]) L [ C ] = [ C ] + merge ( L [ B 1 ​ ] , L [ B 2 ​ ] , … , L [ B n ​ ] , [ B 1 ​ , B 2 ​ , … , B n ​ ])

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) ⟹ L [ A ] = [ A , object ] \implies L[A] = [A, \text{object}] ⟹ L [ A ] = [ A , object ]
  • class B(A) ⟹ L [ B ] = [ B , A , object ] \implies L[B] = [B, A, \text{object}] ⟹ L [ B ] = [ B , A , object ]
  • class C(A) ⟹ L [ C ] = [ C , A , object ] \implies L[C] = [C, A, \text{object}] ⟹ L [ C ] = [ C , A , object ]
  • class D(B, C)

Calculation for L [ D ] 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 ( 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]) L [ D ] = [ D ] + merge ([ B , A , object ] , [ C , A , object ] , [ B , C ])

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

Candidate A A A : Head of list 1. Is A A A in the tail of list 2 ( [A, object] )? Yes ( A A A is in the tail after C C C !). Skip A A A .

Candidate C C C : Head of list 2. Is C C C in the tail of any list? No. ⟹ \implies ⟹ Extract C C C . L [ D ] = [ D , B , C ] + merge ( [ A , object ] , [ A , object ] ) L[D] = [D, B, C] + \text{merge}([A, \text{object}], [A, \text{object}]) L [ D ] = [ D , B , C ] + merge ([ A , object ] , [ A , object ])

Candidate A A A : Head of list 1. Not in tail of any list. ⟹ \implies ⟹ Extract A A A . L [ D ] = [ D , B , C , A ] + merge ( [ object ] , [ object ] ) L[D] = [D, B, C, A] + \text{merge}([\text{object}], [\text{object}]) L [ D ] = [ D , B , C , A ] + merge ([ object ] , [ object ])

Candidate object \text{object} object : Extract object \text{object} object . Final MRO: D ⟶ B ⟶ C ⟶ A ⟶ object \text{Final MRO: } D \longrightarrow B \longrightarrow C \longrightarrow A \longrightarrow \text{object} Final MRO: D ⟶ B ⟶ C ⟶ A ⟶ object

Notice how C C C is visited before A A A , ensuring that C C C 's overrides take precedence over A A 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 immutable tuple of class objects.

  • 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 .