Swiftorial Logo
Home
Swift Lessons
Tutorials
Learn More
Career
Resources

Python FAQ: Top Questions

21. Explain Python's Method Resolution Order (MRO). How is it determined?

The **Method Resolution Order (MRO)** in Python is the sequence in which Python searches for methods and attributes in a class hierarchy, especially when dealing with inheritance. It dictates the order in which base classes are searched when a method is called on an instance of a class. Understanding the MRO is crucial for predicting how methods will be resolved in complex inheritance structures, particularly with multiple inheritance.

Purpose of MRO:

  • Deterministic Method Lookup: Ensures a consistent and predictable order for method and attribute lookup, preventing ambiguity when methods with the same name exist in different parent classes.
  • Supporting Cooperative Multiple Inheritance: Enables the correct functioning of `super()` by providing a clear, linear order for delegating calls up the inheritance chain, ensuring all relevant parent methods (especially `__init__`) are called exactly once.
  • Resolving the "Diamond Problem": In a diamond inheritance pattern (where a class inherits from two classes that both inherit from a common ancestor), the MRO ensures the common ancestor's methods are called only once.

How MRO is Determined:

Python 2 used a depth-first, left-to-right approach for MRO, which had limitations with multiple inheritance. Python 3 (and new-style classes in Python 2.2+) uses the **C3 linearization algorithm** to determine the MRO. C3 linearization guarantees several properties:

  • Consistency: The MRO is consistent across all instances of a class.
  • Monotonicity: The order of classes in the MRO of a class's superclasses is preserved in the class's own MRO. This means if class A precedes class B in a superclass's MRO, it will also precede B in the subclass's MRO.
  • Local Precedence Order: A class always comes before its parent classes. Also, in multiple inheritance, a class always comes before the classes to its right in the base class list.
  • Directed Acyclic Graph (DAG): The class hierarchy must form a DAG (no circular inheritance).

The C3 linearization algorithm essentially works by constructing a list of the class itself, followed by a merged list of its direct superclasses' MROs, and then the list of its direct superclasses, while ensuring that local precedence and monotonicity are maintained.

Inspecting the MRO:

You can view the MRO of any class using:

  • `ClassName.__mro__`: This is a tuple containing the class itself and all its parent classes in the MRO order.
  • `ClassName.mro()`: This is a method that returns the same MRO list.
  • `help(ClassName)`: The help documentation for a class often includes its MRO.

Understanding the MRO is fundamental to mastering complex class hierarchies and using `super()` effectively for cooperative inheritance.


# --- Example 1: Single Inheritance MRO ---
print("--- Single Inheritance MRO ---")

class Animal:
    def speak(self):
        print("Animal makes a sound.")

class Dog(Animal):
    def speak(self):
        print("Woof!")

class GoldenRetriever(Dog):
    def speak(self):
        print("Golden Retriever barks softly.")

print("MRO for Animal:", Animal.__mro__)
print("MRO for Dog:", Dog.__mro__)
print("MRO for GoldenRetriever:", GoldenRetriever.__mro__)

goldie = GoldenRetriever()
goldie.speak() # Calls GoldenRetriever's speak


# --- Example 2: Simple Multiple Inheritance MRO ---
print("\n--- Simple Multiple Inheritance MRO ---")

class Flyer:
    def fly(self):
        print("I can fly!")

class Swimmer:
    def swim(self):
        print("I can swim!")

class Duck(Swimmer, Flyer): # Order matters: Swimmer, then Flyer
    def quack(self):
        print("Quack!")

print("MRO for Duck (Swimmer, Flyer):", Duck.__mro__)
# Expected: Duck, Swimmer, Flyer, object

duck = Duck()
duck.swim()
duck.fly()


# --- Example 3: Diamond Problem MRO (C3 Linearization in action) ---
print("\n--- Diamond Problem MRO ---")

class A:
    def method(self):
        print("Method from A")

class B(A):
    def method(self):
        print("Method from B")
        super().method() # Calls next in MRO

class C(A):
    def method(self):
        print("Method from C")
        super().method() # Calls next in MRO

class D(B, C): # Inherits from B first, then C
    def method(self):
        print("Method from D")
        super().method() # Calls next in MRO

print("MRO for D:", D.__mro__)
# Expected MRO: D, B, C, A, object

d_instance = D()
d_instance.method()
# Output order for d_instance.method() due to MRO and super():
# Method from D
# Method from B
# Method from C
# Method from A


# --- Example 4: MRO for a more complex scenario ---
print("\n--- Complex MRO Example ---")

class X: pass
class Y: pass
class A(X, Y): pass
class B(Y, X): pass # Different order than A

# class C(A, B): pass # This would cause a TypeError (MRO conflict)
# Output: TypeError: Cannot create a consistent method resolution order (MRO) for bases X, Y

# Let's create a solvable one instead
class M:
    def method(self):
        print("Method from M")

class N(M):
    def method(self):
        print("Method from N")
        super().method()

class P(M):
    def method(self):
        print("Method from P")
        super().method()

class Q(N, P):
    def method(self):
        print("Method from Q")
        super().method()

print("MRO for Q:", Q.__mro__)
q_instance = Q()
q_instance.method()
# Expected: Q, N, P, M, object
# Output:
# Method from Q
# Method from N
# Method from P
# Method from M
        

Explanation of the Example Code:

  • **Single Inheritance MRO:**
    • The `__mro__` for `Animal` is simply `(Animal, object)`.
    • For `Dog(Animal)`, it's `(Dog, Animal, object)`.
    • For `GoldenRetriever(Dog)`, it's `(GoldenRetriever, Dog, Animal, object)`.
    • This shows a straightforward linear progression up the inheritance chain.
  • **Simple Multiple Inheritance MRO:**
    • `Duck(Swimmer, Flyer)` demonstrates how the order of base classes in the definition impacts the MRO. `Swimmer` appears before `Flyer` because it was listed first. The MRO is `(Duck, Swimmer, Flyer, object)`.
  • **Diamond Problem MRO:**
    • Classes `B` and `C` both inherit from `A`. Class `D` inherits from both `B` and `C`. This creates a diamond shape.
    • The `D.__mro__` is `(D, B, C, A, object)`. The C3 algorithm ensures that `A` appears only once and at the end of its "branch" after `B` and `C` have been processed, resolving the ambiguity of which `A.method` to call.
    • When `d_instance.method()` is called, `super().method()` in `D` calls `B.method`, then `super().method()` in `B` calls `C.method` (because `C` is next in D's MRO after `B`), and finally `super().method()` in `C` calls `A.method`. This precise order is guaranteed by the MRO.
  • **Complex MRO Example (C3 Consistency):**
    • The commented-out `class C(A, B):` would result in a `TypeError`. This is because `A` puts `X` before `Y` in its MRO (`A(X, Y)`), while `B` puts `Y` before `X` (`B(Y, X)`). When `C` tries to merge these, it finds a conflict: `X` and `Y` appear in different orders in the superclasses. C3 linearization detects this and prevents an inconsistent MRO.
    • The solvable example with `Q(N, P)` shows how `super()` and MRO enable a consistent call chain even in more complex structures where multiple paths lead to a common ancestor (`M`). The call stack follows the MRO: `Q -> N -> P -> M`.

The examples highlight that Python's MRO, determined by the C3 linearization algorithm, provides a robust and predictable mechanism for method lookup in complex inheritance hierarchies, especially crucial for cooperative multiple inheritance using `super()`.