How Python Handles Multiple Inheritance- MRO Explained

Learn to design robust Python class hierarchies by mastering multiple inheritance and Method Resolution Order (MRO) behavior.

Multiple Inheritance in Python is when a class inherits from more than one parent class, allowing it to access methods and attributes from all parent classes. Method Resolution Order (MRO) is the system that determines which method gets called when a class inherits from multiple parent classes that have the same method name.

If you’ve ever wondered why super() works the way it does in complex class hierarchies, or why Python sometimes complains about "inconsistent method resolution order," you need to understand both multiple inheritance and MRO.

Multiple inheritance allows you to combine functionality from different classes, creating powerful and reusable code patterns like mixins. However, when multiple parent classes have methods with the same name, Python needs a systematic way to decide which one to call – that’s where MRO comes in.

The concept of Method Resolution Order using C3 linearization was introduced to Python through the work of Samuele Pedroni, who identified issues with the original algorithm.

1. Understanding Multiple Inheritance

Before we tackle MRO, let’s understand what happens when a class inherits from multiple parents.

Multiple inheritance allows a class to inherit from more than one parent class, giving it access to methods and attributes from all parent classes. This is powerful for code reuse and creating modular designs.

Let’s start with a simple example to see how multiple inheritance works. We’ll create a Logger class that provides logging functionality and a Validator class that provides validation.

PyInteractive Python
class Logger: def log(self): print("Logging activity") class Validator: def validate(self): print("Validating data") class FileProcessor(Logger, Validator): def process(self): print("Processing file") # Test multiple inheritance processor = FileProcessor() processor.process() # FileProcessor's own method processor.log() # From Logger processor.validate() # From Validator #> Processing file #> Logging activity #> Validating data

  

This shows how FileProcessor inherits functionality from both parent classes without any conflicts since each parent has different method names.

When Multiple Inheritance Gets Complicated

Now let’s see what happens when multiple parent classes have methods with the same name. We’ll create two classes that both have a save() method.

PyInteractive Python
class FileHandler: def save(self): print("Saving to file") class CloudHandler: def save(self): print("Saving to cloud") class DataProcessor(FileHandler, CloudHandler): pass # Which save method gets called? processor = DataProcessor() processor.save() #> Saving to file

  

When you run this code, you’ll get "Saving to file" because FileHandler comes first in the inheritance list. But how does Python decide this systematically? This is where Method Resolution Order becomes crucial.

Understanding Inheritance Order

The order in which you list parent classes matters in multiple inheritance. Let’s see this in action by creating the same class but with different parent class orders

PyInteractive Python
class ProcessorA(FileHandler, CloudHandler): pass class ProcessorB(CloudHandler, FileHandler): pass proc_a = ProcessorA() proc_b = ProcessorB() print("File first:") proc_a.save() # "Saving to file" print("Cloud first:") proc_b.save() # "Saving to cloud" #> File first: #> Saving to file #> Cloud first: #> Saving to cloud

  

This demonstrates that the order of inheritance directly affects which method gets called when there are naming conflicts.

Method Resolution Order: Python’s Solution

To handle these conflicts systematically, Python uses Method Resolution Order (MRO). Let’s check the MRO for our classes to understand exactly how Python searches for methods

PyInteractive Python
print("ProcessorA MRO:", ProcessorA.__mro__) print("ProcessorB MRO:", ProcessorB.__mro__) #> ProcessorA MRO: (<class '__main__.ProcessorA'>, <class '__main__.FileHandler'>, <class '__main__.CloudHandler'>, <class 'object'>) #> ProcessorB MRO: (<class '__main__.ProcessorB'>, <class '__main__.CloudHandler'>, <class '__main__.FileHandler'>, <class 'object'>)

  

This shows the exact order Python searches for methods in each class. Python stops at the first class that has the requested method.

2. The Diamond Problem

The diamond problem occurs when a class inherits from two classes that both inherit from a common base class. This creates a diamond-shaped inheritance graph.

We’ll create a class A with a method rk(), then classes B and C that both inherit from A, and finally class D that inherits from both B and C.

PyInteractive Python
class A: def rk(self): print("In class A") class B(A): def rk(self): print("In class B") class C(A): def rk(self): print("In class C") class D(B, C): pass r = D() r.rk() # Which method gets called? print("MRO:", D.__mro__) #> In class B #> MRO: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

  

The diamond inheritance looks like this:

  A
 / \
B   C
 \ /
  D

Python follows the order: D -> B -> C -> A -> object. Notice that class A appears only once at the end, even though both B and C inherit from it. This is the C3 linearization algorithm preventing the diamond problem.

3. Old vs New Style Classes: DLR vs C3 Linearization

Before diving into how C3 linearization works, you need to understand the evolution of Python’s method resolution.

Old Style Classes (Python 2.1 and earlier) used the DLR (Depth-First Left-to-Right) algorithm:

PyInteractive Python
# Old style class (Python 2.x) class OldStyleClass: pass

  

New Style Classes (Python 2.2+ and all Python 3.x) use C3 linearization:

PyInteractive Python
# New style class (automatically in Python 3.x) class NewStyleClass(object): pass

  

The Problem with DLR Algorithm

The DLR algorithm had inconsistencies. Let’s see why it was problematic:

class A:
    def method(self):
        return "A"

class B:
    def method(self):
        return "B"

class C(A, B):
    pass

class D(B, A):
    pass

# This would be problematic with DLR
class E(C, D):
    pass

print("E's MRO:", E.__mro__)
#> TypeError: Cannot create a consistent method resolution
#> order (MRO) for bases A, B

The DLR algorithm would create inconsistent resolution orders. Samuele Pedroni first discovered an inconsistency and introduced C3 Linearization algorithm.

4. How C3 Linearization Works

The C3 linearization algorithm works on three fundamental rules:

  1. Inheritance graph determines the structure of method resolution order
  2. You must visit the super class only after the method of the local classes are visited
  3. Monotonicity: If A comes before B in one class’s MRO, A should come before B in all subclasses

The algorithm also follows these constraints:

  • Children precede their parents
  • If a class inherits from multiple classes, they are kept in the order specified in the tuple of the base class

Let’s trace through a more complex example to see how these rules work in practice. We’ll create classes A, B, and C with methods, then class D that inherits from multiple parents:

PyInteractive Python
class A: def method(self): return "A" class B(A): def method(self): return "B" class C(A): def method(self): return "C" class D(B, C): pass print("D's MRO:", D.__mro__) print("D().method():", D().method()) #> D's MRO: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>) #> D().method(): B

  

The MRO for D is: D -> B -> C -> A -> object

This means when we call D().method(), Python checks:

  1. Does D have a method()? No.
  2. Does B have a method()? Yes! Return "B".

5. Methods to Check MRO

Python provides two ways to check the Method Resolution Order of a class:

  1. __mro__ attribute: Returns a tuple of classes in the MRO
  2. mro() method: Returns a list of classes in the MRO

Let’s create a simple inheritance hierarchy and use both methods to check the MRO. We’ll create classes A and B with methods, then class C that inherits from both:

PyInteractive Python
class A: def rk(self): print("In class A") class B: def rk(self): print("In class B") class C(A, B): def __init__(self): print("Constructor C") r = C() # Both methods show the same MRO print("Using __mro__:", C.__mro__) print("Using mro():", C.mro())

  
Constructor C
Using __mro__: (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
Using mro(): [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]

The difference is that __mro__ returns a tuple while mro() returns a list, but both contain the same information.

6. Understanding super() and MRO

The super() function follows the MRO to find the next method in the chain. This is crucial for cooperative inheritance.

Let’s create three mixins that work together using super() to demonstrate how the MRO chain works. We’ll create a LoggingMixin that logs before and after processing, a DatabaseMixin that handles database operations, and a BaseProcessor that does the actual work:

PyInteractive Python
class LoggingMixin: def process(self): print("Starting process") result = super().process() print("Finished process") return result class DatabaseMixin: def process(self): print("Processing database operation") result = super().process() print("Database operation complete") return result class BaseProcessor: def process(self): print("Base processing") return "Processed" class DataProcessor(LoggingMixin, DatabaseMixin, BaseProcessor): pass # Let's see what happens processor = DataProcessor() print("MRO:", DataProcessor.__mro__) print("\nCalling process():") result = processor.process() print(f"Result: {result}") #> MRO: (<class '__main__.DataProcessor'>, <class '__main__.LoggingMixin'>, <class '__main__.DatabaseMixin'>, <class '__main__.BaseProcessor'>, <class 'object'>) #> #> Calling process(): #> Starting process #> Processing database operation #> Base processing #> Database operation complete #> Finished process #> Result: Processed

  

This demonstrates how super() follows the MRO to create a chain of method calls. Each class’s process() method calls super().process() to continue down the MRO chain.

7. Common MRO Pitfalls and Solutions

Pitfall 1: Inconsistent Method Resolution Order

Let’s create a scenario where Python can’t create a consistent MRO and will raise a TypeError. We’ll create classes with conflicting inheritance orders:

PyInteractive Python
class A: pass class B(A): pass class C(A): pass class D(B, A): pass # This will work fine print("D's MRO:", D.__mro__) # But this will fail try: class E(C, D): pass except TypeError as e: print(f"Error: {e}") #> D's MRO: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

  

This fails because there’s no way to create a consistent linearization that respects all the inheritance relationships.

Pitfall 2: Forgetting to Call super()

Let’s create an example where we override a method in multiple inheritance but forget to call super(), breaking the chain. We’ll create a ValidationMixin that validates data, a LoggingMixin that logs operations, and a BaseModel that saves data:

PyInteractive Python
class ValidationMixin: def save(self): print("Validating data") super().save() class LoggingMixin: def save(self): print("Logging save operation") super().save() class BaseModel: def save(self): print("Saving to database") class User(ValidationMixin, LoggingMixin, BaseModel): def save(self): print("User-specific save logic") super().save() # This is crucial! user = User() user.save() #> User-specific save logic #> Validating data #> Logging save operation #> Saving to database

  

Without the super().save() call in the User class, the chain would break and the mixins wouldn’t execute.

8. Best Practices for Multiple Inheritance

  1. Use mixins for shared behavior: Keep mixins focused on a single responsibility
  2. Always call super(): Maintain the method resolution chain
  3. Be explicit about method order: List parent classes in the order you want methods resolved
  4. Use init_subclass for validation: Check MRO consistency when needed

Let’s create an example of using __init_subclass__ to validate MRO consistency. We’ll create a ValidatedMixin that ensures it appears before BaseModel in the MRO:

PyInteractive Python
class ValidatedMixin: def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) # Check that this mixin appears before BaseModel in MRO mro = cls.__mro__ if BaseModel in mro: validated_index = mro.index(ValidatedMixin) base_index = mro.index(BaseModel) if validated_index > base_index: raise TypeError("ValidatedMixin must come before BaseModel") class BaseModel: def save(self): return "Saved" class User(ValidatedMixin, BaseModel): pass # This would raise a TypeError: # class BadUser(BaseModel, ValidatedMixin): # pass