Python Method Resolution Order Explained
python method resolution order: Understand how Python determines which method to call in multiple inheritance, how C3 linearization works, and how to use super() corre...
When a class inherits from multiple parents, Python must decide which method to call when the same name appears in more than one ancestor. That decision is governed by the python method resolution order, or MRO. The MRO is a linear ordering of the class and its ancestors that determines attribute lookup, method calls, and the behavior of super(). Misunderstanding it leads to subtle bugs, especially in diamond-shaped inheritance hierarchies.
What the MRO Is and Why It Matters
Every Python class has an MRO: a tuple of classes that defines the order in which attribute lookups are performed. When you write instance.method(), Python searches the MRO for the first class that defines method. The same order applies to attribute access and to super() calls.
For single inheritance, the MRO is straightforward: the class itself, then its parent, then the grandparent, and so on up to object. Multiple inheritance makes the order less obvious because a class can be reachable through several paths. Python uses a specific algorithm to compute a consistent, monotonic order that respects the local precedence of each base class list.
How C3 Linearization Works
Python's MRO is computed using C3 linearization, also known as C3 superclass linearization. The algorithm merges the linearizations of the base classes while preserving two constraints:
- The local precedence order of the bases as listed in the class definition.
- Monotonicity: if class A appears before class B in the MRO of a class, then A must also appear before B in the MRO of any subclass that inherits from that class.
Consider this example:
class A: def method(self): print("A") class B(A): def method(self): print("B") class C(A): def method(self): print("C") class D(B, C): pass
The MRO for D is [D, B, C, A, object]. C3 builds this by merging the linearizations of B and C. The order B before C reflects the local order in class D(B, C). The algorithm then places A after both because it appears in both parent linearizations and must come after them to maintain monotonicity.
Using super() Correctly in a Multiple Inheritance Hierarchy
super() returns a proxy object that delegates method calls to the next class in the MRO. In a multiple inheritance hierarchy, super() is not just a reference to the parent class; it is dynamically bound to the MRO of the instance that calls it. This makes cooperative multiple inheritance possible when every class in the hierarchy uses super() consistently.
Consider a cooperative hierarchy:
class Base: def __init__(self, value): self.value = value print("Base") class Left(Base): def __init__(self, value): n super().__init__(value) print("Left") class Right(Base): def __init__(self, value): super().__init__(value) print("Right") class Child(Left, Right): def __init__(self, value): super().__init__(value) print("Child")
When you create Child(10), the MRO is [Child, Left, Right, Base, object]. The super() calls in Child, Left, and Right each pass control to the next class in that order, so Base.__init__ runs exactly once. If any class in the chain does not call super(), the chain breaks and subsequent initializers are skipped.
The Diamond Problem and How MRO Resolves It
A diamond occurs when a class inherits from two classes that share a common ancestor. For example:
class Root: def identify(self): return "root" class Left(Root): pass class Right(Root): pass class Leaf(Left, Right): pass
Without a defined MRO, a naive depth-first search would call Root.identify twice or in an inconsistent order. Python's C3 linearization ensures that Root appears only once and after both Left and Right. The MRO of Leaf is [Leaf, Left, Right, Root, object]. When you call Leaf().identify(), Python finds Root.identify after checking Leaf, Left, and Right. The key benefit is that Root is not visited before Left or Right, preserving the intended precedence.
Inspecting the MRO at Runtime
You can always inspect the MRO of a class using the __mro__ attribute or the mro() method:
print(Leaf.__mro__) # (<class '__main__.Leaf'>, <class '__main__.Left'>, <class '__main__.Right'>, <class '__main__.Root'>, <class 'object'>)
The __mro__ tuple is read-only and is computed when the class is created. It is also used by isinstance() and issubclass() checks. If you ever need to understand why a particular method is being called, printing __mro__ is the first diagnostic step.
Common MRO Pitfalls and How to Avoid Them
One frequent mistake is assuming that super() always refers to the direct parent. In a multiple inheritance hierarchy, super() follows the MRO, which may not be the same as the class's immediate base. This can cause surprising behavior if some classes in the hierarchy are not designed cooperatively.
Another pitfall is mixing classes that use super() with classes that explicitly call the parent class by name. For example, if Left calls Base.__init__(self) instead of super().__init__(), the MRO chain is broken, and Right.__init__ will never run. In cooperative hierarchies, every class must use super() consistently.
A third issue arises when the base class order in a class definition is changed. The MRO depends on the order of bases, so reordering class D(B, C) to class D(C, B) changes the MRO and therefore the method resolution. This can silently alter behavior in existing code.
MRO and Class Creation: Where the Cost Lives
The MRO is computed once at class definition time, not on every method call. The C3 algorithm runs when the class is created and stores the resulting tuple. This means there is no runtime lookup overhead for the MRO itself; attribute access uses the precomputed tuple. The cost of computing the MRO is negligible for typical class hierarchies, but it can become noticeable if you generate many classes dynamically with complex inheritance structures. In practice, the MRO calculation is a one-time cost and does not affect the performance of method dispatch.
Understanding the MRO is essential for designing maintainable multiple inheritance hierarchies. It lets you predict which method will be called, use super() effectively, and avoid the subtle bugs that arise from ambiguous method resolution.