Python Method Resolution Order (MRO): A Comprehensive Deep Dive

In Python, the Method Resolution Order (MRO) is the mechanism that determines the order in which base classes are searched when looking for a method or attribute in a class hierarchy. This is especially critical in the context of inheritance, particularly with multiple inheritance, where ambiguity could arise. Python uses the C3 linearization algorithm to compute the MRO, ensuring a consistent and predictable resolution path. In this blog, we’ll explore what MRO is, how it works, practical examples, its features, and its significance in Python’s object-oriented programming.


What Is Method Resolution Order (MRO)?

link to this section

The Method Resolution Order is a sequence of classes that Python follows to resolve a method or attribute call on an object. It defines the lookup path from the instance’s class through its superclasses up to the root object class.

Key Concepts

  • Inheritance Hierarchy : MRO applies when a class inherits from one or more base classes.
  • C3 Linearization : The algorithm Python uses to compute a linear order, balancing depth-first and left-to-right traversal.
  • Consistency : Ensures predictable behavior, even in complex multiple inheritance scenarios.

Example

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

d = D()
print(d.method())  # Output: B
print(D.__mro__)   # Output: (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

How MRO Works in Python

link to this section

Defining the MRO

  • The MRO is calculated when a class is defined and stored in the __mro__ attribute (a tuple of classes).
  • Python uses the C3 linearization algorithm, which:
    1. Starts with the class itself.
    2. Considers the order of base classes (left-to-right).
    3. Respects the inheritance hierarchy (depth-first where possible).
    4. Avoids duplicates and ensures each class appears only once.

Basic Structure

class X:
    def who(self):
        return "X"

class Y(X):
    def who(self):
        return "Y"

class Z(Y):
    pass

z = Z()
print(z.who())      # Output: Y
print(Z.__mro__)    # Output: (<class 'Z'>, <class 'Y'>, <class 'X'>, <class 'object'>)

Accessing the MRO

  • __mro__ : Class attribute showing the resolution order.
  • mro() : Class method returning the same as a list.

Example

print(Z.mro()) # Output: [<class 'Z'>, <class 'Y'>, <class 'X'>, <class 'object'>]

MRO in Action

  • When a method is called (e.g., obj.method()), Python:
    1. Looks at the instance’s class.
    2. Follows the MRO to find the first class defining the method.
    3. Executes that method.

Features of MRO

link to this section

1. Single Inheritance

In simple hierarchies, MRO is straightforward:

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

class B(A):
    pass

b = B()
print(b.info())     # Output: A
print(B.__mro__)    # Output: (<class 'B'>, <class 'A'>, <class 'object'>)

2. Multiple Inheritance

MRO resolves ambiguity in complex hierarchies:

class A:
    def greet(self):
        return "Hello from A"

class B(A):
    def greet(self):
        return "Hello from B"

class C(A):
    pass

class D(B, C):
    pass

d = D()
print(d.greet())    # Output: Hello from B
print(D.__mro__)    # Output: (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

3. Diamond Problem

MRO handles the classic diamond problem predictably:

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

class B(A):
    pass

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

class D(B, C):
    pass

d = D()
print(d.method())   # Output: C
print(D.__mro__)    # Output: (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
  • C precedes A because it’s listed later in D(B, C).

4. Using super()

super() follows the MRO to call the next method in the chain:

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

class B(A):
    def step(self):
        return f"B step, {super().step()}"

class C(B):
    pass

c = C()
print(c.step())     # Output: B step, A step
print(C.__mro__)    # Output: (<class 'C'>, <class 'B'>, <class 'A'>, <class 'object'>)

Practical Examples

link to this section

Example 1: Simple Hierarchy

class Vehicle:
    def move(self):
        return "Moving"

class Car(Vehicle):
    def move(self):
        return "Driving"

c = Car()
print(c.move())     # Output: Driving
print(Car.__mro__)  # Output: (<class 'Car'>, <class 'Vehicle'>, <class 'object'>)

Example 2: Multiple Inheritance

class Flyer:
    def travel(self):
        return "Flying"

class Driver:
    def travel(self):
        return "Driving"

class Hybrid(Flyer, Driver):
    pass

h = Hybrid()
print(h.travel())      # Output: Flying
print(Hybrid.__mro__)  # Output: (<class 'Hybrid'>, <class 'Flyer'>, <class 'Driver'>, <class 'object'>)

Example 3: Diamond with Super

class Base:
    def process(self):
        return "Base"

class Left(Base):
    def process(self):
        return f"Left, {super().process()}"

class Right(Base):
    def process(self):
        return f"Right, {super().process()}"

class Bottom(Left, Right):
    def process(self):
        return f"Bottom, {super().process()}"

b = Bottom()
print(b.process())      # Output: Bottom, Left, Right, Base
print(Bottom.__mro__)   # Output: (<class 'Bottom'>, <class 'Left'>, <class 'Right'>, <class 'Base'>, <class 'object'>)

Example 4: Mixin Pattern

class Loggable:
    def log(self):
        return "Logging"

class Worker:
    def work(self):
        return "Working"

class Robot(Worker, Loggable):
    def work(self):
        return f"{super().work()}, {self.log()}"

r = Robot()
print(r.work())       # Output: Working, Logging
print(Robot.__mro__)  # Output: (<class 'Robot'>, <class 'Worker'>, <class 'Loggable'>, <class 'object'>)

Performance Implications

link to this section

Overhead

  • Lookup Cost : MRO introduces a small overhead for method resolution, proportional to hierarchy depth.
  • Cached : MRO is computed once at class definition, not per call.

Benchmarking

import time

class A:
    def m(self):
        pass

class B(A):
    pass

class C(B):
    pass

c = C()
start = time.time()
for _ in range(1000000):
    c.m()
print(time.time() - start)  # Minimal overhead

Memory

  • Lightweight : MRO is stored as a tuple in __mro__, negligible memory impact.

MRO vs. Other Constructs

link to this section
  • Single Inheritance : MRO is trivial, just a linear chain.
  • Multiple Inheritance : MRO shines, resolving complexity other languages might restrict.
  • Manual Resolution : Without MRO, developers would need explicit calls, reducing flexibility.

Practical Use Cases

link to this section
  1. Framework Design :
    class BaseHandler:
        def handle(self):
            return "Base"
    
    class CustomHandler(BaseHandler):
        def handle(self):
            return "Custom"
  2. Mixin Classes :
    class Printable:
        def print(self):
            return "Printing"
    
    class Device(Printable):
        pass
  3. Plugin Systems :
    class Plugin:
        def execute(self):
            pass
    
    class BackupPlugin(Plugin):
        def execute(self):
            return "Backup"
  4. Superclass Coordination :
    class A:
        def init(self):
            return "A init"
    
    class B(A):
        def init(self):
            return f"B, {super().init()}"

Edge Cases and Gotchas

link to this section

1. Invalid MRO

class A: pass
class B(A): pass
class C(A, B): pass  # TypeError: Cannot create a consistent method resolution order
  • MRO fails if the hierarchy is inconsistent.

2. Super() Behavior

class X:
    def m(self):
        return "X"

class Y(X):
    def m(self):
        return super().m()

y = Y()
print(y.m())  # Output: X

3. Overriding in Middle

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

class B(A):
    def x(self):
        return "B"

class C(B):
    pass

c = C()
print(c.x())  # Output: B

4. Diamond Ambiguity

class Top:
    def m(self):
        return "Top"

class Left(Top): pass
class Right(Top):
    def m(self):
        return "Right"

class Bottom(Left, Right): pass

b = Bottom()
print(b.m())  # Output: Right

Conclusion

link to this section

Python’s Method Resolution Order, powered by the C3 linearization algorithm, is a robust system for navigating class hierarchies, especially in multiple inheritance scenarios. By defining a clear, predictable path through __mro__, it resolves method lookups dynamically and consistently. From simple single inheritance to complex diamond patterns, MRO ensures flexibility and clarity in object-oriented design. Understanding its mechanics—how it orders classes and interacts with super()—empowers you to leverage inheritance effectively, building scalable, maintainable Python code.