Menu

How Python Handles Multiple Inheritance- MRO Explained

Join thousands of students who advanced their careers with MachineLearningPlus. Go from Beginner to Data Science (AI/ML/Gen AI) Expert through a structured pathway of 9 core specializations and build industry grade projects.

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.

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.

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

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

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.

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:

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

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

# 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:

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:

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:

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:

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:

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:

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

Scroll to Top
Course Preview

Machine Learning A-Z™: Hands-On Python & R In Data Science

Free Sample Videos:

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Scroll to Top