Python Multiple Inheritance Method Resolution Explained
python multiple inheritance method resolution: Understand Python's MRO for multiple inheritance: how C3 linearization works, how to use super() correctly, and how to a...
python multiple inheritance method resolution requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When a class inherits from multiple parents, Python must decide which method to call when a name is ambiguous. The method resolution order (MRO) is the sequence of classes that Python checks to find an attribute or method. For single inheritance, the order is straightforward: the child, then the parent, then the base. For multiple inheritance, the order is determined by the C3 linearization algorithm, which preserves monotonicity and local precedence. Understanding python multiple inheritance method resolution is essential for writing predictable cooperative hierarchies and debugging obscure attribute errors.
The C3 Linearization Algorithm
Python computes the MRO for every class at class definition time using the C3 linearization algorithm. The algorithm merges the MROs of all parent classes with the parents themselves, following two constraints:
- A class always appears before its parents.
- If a class appears in multiple parent MROs, its relative order is preserved across all of them.
The result is a single, consistent linear order. Consider the classic diamond pattern:
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]. The C3 algorithm merges the parent MROs [B, A, object] and [C, A, object] with the parent list [B, C], yielding this order. When you call D().method(), Python finds method in B first and prints "B".
You can inspect the MRO directly using the __mro__ attribute:
print(D.__mro__) # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
How super() Works with MRO
super() in a class method does not simply call the parent class's method. It uses the MRO of the instance's class to find the next class after the one where the method is defined. This is crucial in multiple inheritance because a direct call to Parent.method(self) can bypass the MRO and break cooperative behavior.
Consider the following cooperative hierarchy:
class A: def process(self): print("A") class B(A): def process(self): print("B") super().process() class C(A): def process(self): print("C") super().process() class D(B, C): def process(self): print("D") super().process()
The MRO of D is [D, B, C, A, object]. When you call D().process(), the output is:
D
B
C
A
Each super().process() call resolves to the next class in the MRO, not necessarily the immediate parent. This is what makes cooperative multiple inheritance possible: each class can contribute its behavior and then delegate to the next class in the chain.
Common MRO Pitfalls
Inconsistent Parent Order
If you declare a class with parents whose MROs conflict, Python raises a TypeError at class definition time. For example:
class X: pass class Y(X): pass class Z(X, Y): pass
This fails because X appears before Y in the parent list, but Y already inherits from X, so X must come after Y in the merged MRO. The C3 algorithm cannot produce a consistent order, so Python raises:
TypeError: Cannot create a consistent method resolution order (MRO) for bases X, Y
Misusing super() in Non-Cooperative Classes
If a class in the hierarchy does not call super(), the chain breaks. For example:
class A: def process(self): print("A") class B(A): def process(self): print("B") # No super().process() call class C(A): def process(self): print("C") super().process() class D(B, C): def process(self): print("D") super().process()
Calling D().process() prints D and B, then stops. C and A are never reached because B does not delegate. This is a common source of subtle bugs in frameworks that rely on cooperative inheritance.
MRO and the Diamond Problem
Multiple inheritance often leads to the diamond problem: a class inherits from two classes that share a common ancestor. The MRO resolves this by ensuring the shared ancestor appears only once and after all its subclasses. The exact position depends on the order of base classes in the child definition.
For example, changing the order of bases in D changes the MRO:
class D(C, B): pass
The MRO becomes [D, C, B, A, object]. This affects which method is called first and how super() chains behave. Always consider the base class order as part of your design, not an implementation detail.
Performance and Maintainability Considerations
The MRO is computed once at class creation and stored in __mro__. Attribute lookups follow this linear sequence, so the cost of a method call is proportional to the number of classes in the hierarchy. In typical applications, the MRO length is small, so the overhead is negligible. However, very deep or wide hierarchies can make attribute resolution slightly slower, especially when combined with __getattr__ or dynamic dispatch.
From a maintainability perspective, a long MRO makes code harder to reason about. If you find yourself writing classes with more than two or three parents, consider whether composition or mixins would be clearer. Mixins are a common pattern where each mixin provides a small set of methods and relies on cooperative super() calls to combine behavior.
Debugging MRO Issues
When a method does not behave as expected, inspect the MRO directly. The inspect module can help:
import inspect print(inspect.getmro(D)) # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
You can also use the __mro__ attribute in a debugger. If a method call is skipped, check whether each class in the MRO calls super() with the same method name. A missing super() call is the most common reason for a broken chain.
Another useful technique is to temporarily add print statements in each method to trace the call order. This is often faster than reading the entire MRO when you are dealing with a large hierarchy.
Compatibility Across Python Versions
The C3 linearization algorithm has been stable since Python 2.3, so the MRO behavior is consistent across modern Python 3.x versions. However, the exact error messages and edge cases have changed slightly. For example, Python 3.10 introduced a more precise error message for inconsistent MROs. If you maintain code that supports multiple Python versions, avoid relying on error message text and instead ensure your class hierarchies are consistent by design.
One subtle compatibility concern is the interaction between super() and __class__ cell. In Python 3, super() without arguments relies on the compiler inserting a reference to __class__. This works correctly in methods defined inside a class body, but it fails if you try to use super() in a method that is dynamically created or assigned after class creation. In such cases, pass the class and instance explicitly: super(CurrentClass, self). This is rarely needed but worth knowing when working with metaprogramming.
Building a Cooperative Mixin Chain
A practical application of MRO is building a chain of mixins that each add a behavior. The following example shows a logging mixin and a timing mixin that both call super():
class Base: def run(self): print("Base.run") class LoggingMixin: def run(self): print("LoggingMixin: before") super().run() print("LoggingMixin: after") class TimingMixin: def run(self): print("TimingMixin: start") super().run() print("TimingMixin: end") class Worker(LoggingMixin, TimingMixin, Base): pass Worker().run()
The MRO of Worker is [Worker, LoggingMixin, TimingMixin, Base, object]. The output is:
LoggingMixin: before
TimingMixin: start
Base.run
TimingMixin: end
LoggingMixin: after
This pattern works because each mixin calls super().run(), which continues down the MRO. The order of mixins in the base class list determines the order of execution. If you reverse LoggingMixin and TimingMixin, the output order changes accordingly. This is a powerful way to compose behaviors without deep inheritance trees, but it requires every class in the chain to cooperate by calling super().