Python MRO Explained: Method Resolution Order
python mro: Learn how Python's MRO determines method lookup in multiple inheritance, how C3 linearization works, and how to use super() correctly.
When a class inherits from multiple base classes, Python must decide which method to call when several ancestors define the same name. That decision is made by the python mro (method resolution order), a linear ordering of the class and its ancestors. The MRO is not just a list of classes in inheritance order; it follows a specific algorithm that guarantees consistency and predictability. Understanding this order is essential when you work with multiple inheritance, use super(), or debug surprising attribute lookups.
What Python MRO Determines
Every Python class has an __mro__ attribute that lists the class itself, its ancestors, and object in the order Python uses for attribute and method lookup. When you access instance.method(), Python walks the MRO from left to right and returns the first class that defines method. If no class defines it, AttributeError is raised.
Consider this simple hierarchy:
class A: def greet(self): return "A" class B(A): pass class C(A): def greet(self): return "C" class D(B, C): pass print(D.__mro__)
The output is (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>). Python looks for greet in D, then B, then C, and finds it in C. If C did not define greet, Python would continue to A. The MRO ensures that each class appears exactly once and that every class appears before its own bases, preserving a consistent order.
How C3 Linearization Builds the MRO
Python uses the C3 linearization algorithm to compute the MRO. C3 merges the linearizations of the base classes while respecting two constraints: each class must appear after all of its subclasses, and the local precedence order of bases in the class definition must be preserved. The algorithm starts with the class itself, then recursively processes the bases, merging their MROs.
A common way to understand C3 is to think of it as a topological sort with specific tie-breaking rules. For a class C(B1, B2, ..., Bn), the MRO is [C] + merge(mro(B1), mro(B2), ..., mro(Bn), [B1, B2, ..., Bn]). The merge operation picks the first head of any list that does not appear in the tail of another list, removes it, and repeats. If no such head exists, the hierarchy is inconsistent and Python raises TypeError.
This rule prevents a class from appearing before its own base, which would break method resolution. It also ensures that the order of bases in the class definition is respected, so class D(B, C) gives B priority over C.
Tracing an MRO with a Diamond Inheritance Example
The classic diamond problem illustrates why the MRO matters. Suppose A defines a method, and B and C both inherit from A. A class D inherits from both B and C. Without a defined resolution order, it would be ambiguous which version of the method to call. Python's MRO resolves this by linearizing the hierarchy in a way that keeps each class after its subclasses.
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 d = D() d.method() print(D.__mro__)
Here, D.method() prints B because B appears before C in the MRO. The MRO is [D, B, C, A, object]. Notice that A appears after both B and C, even though B and C both inherit from it. This ordering is not arbitrary; it comes from merging the MROs of B and C with the base list [B, C]. The merge yields B first because B is the head of the first list and does not appear in the tail of any other list. Then C is chosen, and finally A.
If you changed the base order to class D(C, B), the MRO would become [D, C, B, A, object], and D.method() would print C. The base order directly controls which method wins when two ancestors provide conflicting definitions.
How super() Uses the MRO
super() is often misunderstood as a way to call a method on a parent class. In reality, super() returns a proxy object that delegates method calls to the next class in the MRO after the current class. This behavior is what makes cooperative multiple inheritance work.
class A: def __init__(self): print("A init") class B(A): def __init__(self): print("B init") super().__init__() class C(A): def __init__(self): print("C init") super().__init__() class D(B, C): def __init__(self): print("D init") super().__init__() d = D()
Running this code prints D init, B init, C init, and A init. The super() calls in B and C do not directly call A; they call the next class in the MRO. For B, the next class is C, and for C, the next class is A. This chain works only if every class in the hierarchy uses super() consistently and accepts the same arguments. If one class bypasses super() and calls A.__init__() directly, the chain breaks and later classes in the MRO may never be initialized.
A practical rule is to use super() everywhere in a multiple-inheritance hierarchy, even if a class does not have its own base. This keeps the cooperative chain intact and avoids skipping classes.
Common MRO Mistakes and Their Consequences
One frequent mistake is assuming that the MRO matches the order in which base classes are listed in the class definition. While local precedence is respected, the full MRO also depends on the MROs of the bases themselves. Changing a base class's inheritance order can silently change the MRO of a derived class, leading to unexpected method resolution.
Another mistake is calling super() with explicit arguments in a way that breaks the chain. For example, super(B, self).method() explicitly starts the lookup after B in the MRO, which can be useful in rare cases but often leads to fragile code. If the MRO changes, the explicit call may target a different class than intended.
A third issue is diamond inheritance where a method is not designed to cooperate. If a class in the middle of the diamond does not call super(), the chain stops, and classes later in the MRO never receive the call. This is a common source of subtle bugs in frameworks that rely on mixins.
Designing Classes with the MRO in Mind
When you build a class hierarchy with multiple inheritance, keep the MRO predictable. Prefer mixins that are small and focused, and avoid deep inheritance chains when possible. The MRO is computed at class creation time, so you can inspect it with ClassName.__mro__ to verify the order. This is especially useful when you combine several mixins and want to confirm that the intended method is resolved first.
A concrete design pattern is to place mixins before the base class in the inheritance list, so mixin methods take precedence. For example, class LoggedView(LoggingMixin, BaseView) ensures that LoggingMixin methods are found before BaseView methods. This ordering is intuitive and avoids surprising overrides.
Another consideration is that the MRO is fixed once the class is defined. You cannot change it at runtime without redefining the class. If you need dynamic method resolution, you must rely on explicit dispatch or composition instead of inheritance.
MRO and Runtime Cost
The MRO is computed once when a class is created and cached in the __mro__ attribute. Attribute lookups use this cached tuple, so the cost of method resolution is proportional to the number of classes in the MRO, but only at lookup time. For typical hierarchies, the MRO is short, and the overhead is negligible. However, very deep or wide hierarchies can make attribute access slower because Python must iterate through the MRO tuple. In performance-sensitive code, keeping the MRO shallow is a reasonable optimization, but it is rarely the bottleneck.
The more important cost is maintainability. A complex MRO makes it harder to reason about which method will be called. If you find yourself inspecting __mro__ frequently to understand behavior, consider simplifying the hierarchy. Composition often provides clearer control than multiple inheritance, especially when the relationships between classes are not strictly "is-a" relationships.