C3 linearization algorithm, mro() computation, and how Python resolves method lookup across multiple inheritance.
The method resolution order, usually called the MRO, is the ordered list of classes CPython searches when resolving attributes through a class hierarchy.
For a simple class:
class A:
pass
class B(A):
passthe MRO of B is:
print(B.__mro__)Output shape:
(<class '__main__.B'>, <class '__main__.A'>, <class 'object'>)This means CPython searches B, then A, then object.
The MRO is central to inheritance, method lookup, descriptors, super(), multiple inheritance, abstract base classes, and class layout validation.
45.1 Why the MRO Exists
Python supports inheritance. When code asks for an attribute:
obj.nameand the instance does not directly provide it, CPython searches the object’s class and base classes.
Without a defined search order, this expression would be ambiguous:
class A:
def f(self):
return "A"
class B:
def f(self):
return "B"
class C(A, B):
pass
print(C().f())C inherits from both A and B. Both define f.
Python needs a deterministic rule for choosing one.
The MRO gives that rule.
print(C.__mro__)Output shape:
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)So C().f() returns:
Abecause A appears before B.
45.2 The MRO Is Stored on the Class
Every class has an MRO.
class User:
pass
print(User.__mro__)Output shape:
(<class '__main__.User'>, <class 'object'>)The MRO is computed when the class is created and stored on the class object.
It is not recomputed for every attribute access.
You can also call:
print(User.mro())User.__mro__ returns the stored tuple.
User.mro() returns a list-like result computed through class machinery.
For ordinary classes, they contain the same ordering.
45.3 Attribute Lookup Uses the MRO
For:
obj.attrnormal lookup roughly follows:
1. Look at type(obj).
2. Search type(obj).__mro__ in order.
3. Find attr in a class dictionary.
4. Apply descriptor rules if needed.
5. Fall back to instance dictionary or __getattr__ according to lookup rules.Example:
class A:
value = "A"
class B(A):
pass
class C(B):
pass
c = C()
print(c.value)CPython searches:
C
B
A
objectIt finds value in A.
45.4 Method Lookup Uses the Same Rule
Methods are attributes stored on classes.
class A:
def run(self):
return "A.run"
class B(A):
pass
b = B()
print(b.run())The method run is found in A.
Because functions are descriptors, the function object in A.__dict__ is bound to the instance b.
Conceptually:
lookup run in B.__mro__
find A.__dict__["run"]
call function descriptor __get__(b, B)
return bound method
call bound methodThe MRO decides which function object is found first.
45.5 Single Inheritance MRO
Single inheritance is straightforward.
class A:
pass
class B(A):
pass
class C(B):
pass
print(C.__mro__)Output shape:
(C, B, A, object)The search proceeds from most specific to least specific.
child
parent
grandparent
objectThis is the simple case most code relies on.
45.6 Multiple Inheritance MRO
Multiple inheritance requires a more careful algorithm.
class A:
pass
class B:
pass
class C(A, B):
pass
print(C.__mro__)Output shape:
(C, A, B, object)The left-to-right order of base classes matters, but it is not the only rule. Python also preserves ordering constraints from parent classes.
The algorithm used for normal Python classes is C3 linearization.
45.7 C3 Linearization
C3 linearization computes a class order that satisfies three important properties:
local precedence order
monotonicity
consistent extension of parent MROsLocal precedence order means bases listed earlier in the class definition should remain earlier when possible.
class C(A, B):
passmeans A should precede B.
Monotonicity means subclassing should not reorder ancestors in a way that contradicts their existing MROs.
Consistent extension means a class MRO includes the MROs of its bases without breaking their internal ordering.
These rules make multiple inheritance predictable.
45.8 The C3 Merge Model
For a class:
class C(A, B):
passC3 computes:
MRO(C) = [C] + merge(MRO(A), MRO(B), [A, B])The merge operation repeatedly chooses a valid head from the input lists.
A head is valid if it does not appear in the tail of any other list.
This rule prevents choosing a class before another class that must precede it.
45.9 Simple C3 Example
class A:
pass
class B:
pass
class C(A, B):
passInputs:
MRO(A) = [A, object]
MRO(B) = [B, object]
bases = [A, B]So:
MRO(C) = [C] + merge(
[A, object],
[B, object],
[A, B]
)Merge:
choose A because A is not in any other tail
choose B because B is not in any other tail
choose objectResult:
[C, A, B, object]45.10 Diamond Inheritance
The classic multiple inheritance case is the diamond.
class A:
def f(self):
return "A"
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
print(D.__mro__)Output shape:
(D, B, C, A, object)The base class A appears once.
This is important. Python does not produce:
D, B, A, C, A, objectA repeated base would make cooperative method calls difficult and ambiguous.
45.11 Diamond Method Lookup
class A:
def f(self):
return "A"
class B(A):
pass
class C(A):
def f(self):
return "C"
class D(B, C):
pass
print(D().f())
print(D.__mro__)D searches:
D
B
C
A
objectB does not define f.
C defines f.
So the result is:
CEven though B inherits from A, Python does not search all of B before considering C. It searches the linearized MRO.
45.12 MRO and super()
super() follows the MRO.
It does not simply mean “call the parent class.”
Example:
class A:
def f(self):
return "A"
class B(A):
def f(self):
return "B" + super().f()
class C(A):
def f(self):
return "C" + super().f()
class D(B, C):
def f(self):
return "D" + super().f()
print(D().f())
print(D.__mro__)Output:
DBCAThe MRO is:
D, B, C, A, objectSo each super().f() continues after the current class in that order.
45.13 super() Is Dynamic
In this class:
class B(A):
def f(self):
return "B" + super().f()super() does not hard-code A.
If B is used inside another MRO, super() continues after B in that MRO.
Example:
class A:
def f(self):
return "A"
class B(A):
def f(self):
return "B" + super().f()
class C(A):
def f(self):
return "C" + super().f()
class D(B, C):
pass
print(D().f())The call inside B.f goes to C.f, not directly to A.f.
That is the core of cooperative multiple inheritance.
45.14 Cooperative Methods
A cooperative method is written so every class in the MRO can participate.
class A:
def setup(self):
print("A")
super().setup()
class B(A):
def setup(self):
print("B")
super().setup()
class C(A):
def setup(self):
print("C")
super().setup()
class D(B, C):
def setup(self):
print("D")
super().setup()This will eventually fail unless the chain ends with a method that accepts the call.
A common root class:
class Root:
def setup(self):
pass
class A(Root):
def setup(self):
print("A")
super().setup()Now all participants can call super().setup() safely.
45.15 Cooperative __init__
Multiple inheritance commonly fails when __init__ methods do not cooperate.
Bad:
class A:
def __init__(self):
self.a = 1
class B:
def __init__(self):
self.b = 1
class C(A, B):
pass
c = C()
print(hasattr(c, "a"))
print(hasattr(c, "b"))Only A.__init__ runs because C inherits it first.
Better:
class Root:
def __init__(self, **kwargs):
super().__init__()
class A(Root):
def __init__(self, **kwargs):
self.a = 1
super().__init__(**kwargs)
class B(Root):
def __init__(self, **kwargs):
self.b = 1
super().__init__(**kwargs)
class C(A, B):
def __init__(self, **kwargs):
super().__init__(**kwargs)Now C().__dict__ contains both a and b.
45.16 Keyword-Based Cooperative Initialization
A common cooperative pattern passes keyword arguments through the MRO.
class Root:
def __init__(self, **kwargs):
if kwargs:
raise TypeError(f"unexpected arguments: {kwargs}")
super().__init__()
class NameMixin(Root):
def __init__(self, *, name, **kwargs):
self.name = name
super().__init__(**kwargs)
class AgeMixin(Root):
def __init__(self, *, age, **kwargs):
self.age = age
super().__init__(**kwargs)
class User(NameMixin, AgeMixin):
def __init__(self, **kwargs):
super().__init__(**kwargs)
u = User(name="Ada", age=36)
print(u.name)
print(u.age)Each class consumes the arguments it owns and forwards the rest.
This pattern works because all classes agree to cooperate.
45.17 Non-Cooperative Base Classes
Some classes do not call super().
class A:
def setup(self):
print("A")If A appears before other classes in the MRO, it may stop the chain.
class B:
def setup(self):
print("B")
super().setup()
class C(A, B):
pass
C().setup()Only A may run.
In multiple inheritance, every class in the chain must participate. One non-cooperative class can break the whole method chain.
45.18 MRO Conflicts
Some inheritance graphs cannot produce a valid C3 MRO.
Example:
class A:
pass
class B:
pass
class X(A, B):
pass
class Y(B, A):
pass
class Z(X, Y):
passThis fails.
X requires:
A before BY requires:
B before AZ cannot satisfy both.
CPython raises:
TypeError: Cannot create a consistent method resolution orderThis is a design error in the class hierarchy.
45.19 Local Precedence Order
Local precedence order means the order of bases in a class definition matters.
class C(A, B):
passstates that A should precede B.
Changing the base order changes the MRO.
class C1(A, B):
pass
class C2(B, A):
pass
print(C1.__mro__)
print(C2.__mro__)Base order is therefore part of the class API.
45.20 Monotonicity
Monotonicity means subclassing a class should not reorder that class’s ancestors.
If A precedes B in a parent’s MRO, a subclass should not silently reverse them.
This property makes subclass behavior predictable. Adding a subclass should not change the meaning of a parent class’s method resolution assumptions.
C3 enforces this.
45.21 object at the End
For ordinary new-style classes, object appears at the end of the MRO.
class User:
pass
print(User.__mro__)Output shape:
(User, object)object provides fundamental behavior such as:
__new__
__init__
__repr__
__str__
__eq__
__hash__
__getattribute__
__setattr__Even a class with no explicit base inherits from object.
45.22 MRO and Descriptors
Descriptors are found through MRO lookup.
class A:
@property
def value(self):
return "A"
class B(A):
pass
print(B().value)The property descriptor is found in A.__dict__.
Then descriptor logic calls:
A.__dict__["value"].__get__(instance, B)The owner type passed to the descriptor is usually the actual class involved in access, such as B.
This matters for descriptors that inspect objtype.
45.23 Data Descriptor Precedence and MRO
If a data descriptor appears in a base class, it can override an instance dictionary entry.
class A:
@property
def value(self):
return "from property"
class B(A):
pass
b = B()
b.__dict__["value"] = "from dict"
print(b.value)Output:
from propertyThe descriptor is found through the MRO, and because it is a data descriptor, it wins over the instance dictionary.
45.24 Non-Data Descriptor and MRO
Methods are non-data descriptors.
class A:
def f(self):
return "method"
class B(A):
pass
b = B()
b.__dict__["f"] = lambda: "instance"
print(b.f())Output:
instanceThe instance dictionary can shadow the method because function descriptors are non-data descriptors.
The MRO finds the method, but descriptor precedence still allows the instance dictionary to win.
45.25 MRO and Class Attribute Lookup
For class attribute lookup:
B.attrCPython searches B.__mro__.
class A:
x = 1
class B(A):
pass
print(B.x)The attribute x is found in A.
Class lookup also interacts with metaclass lookup. If the class itself does not provide the attribute, the metaclass may provide descriptors or methods.
45.26 MRO and Metaclasses
The class object has its own type, the metaclass.
class Meta(type):
def describe(cls):
return cls.__name__
class User(metaclass=Meta):
pass
print(User.describe())describe is found on Meta, not in User.__mro__.
There are two related lookup structures:
instance attribute lookup:
instance -> class MRO
class object attribute lookup:
class object -> class MRO and metaclass machineryMetaclasses have their own MRO too:
print(type(User).__mro__)45.27 MRO and Abstract Base Classes
Abstract base classes participate in normal inheritance.
from abc import ABC, abstractmethod
class Store(ABC):
@abstractmethod
def get(self, key):
pass
class MemoryStore(Store):
def get(self, key):
return None
print(MemoryStore.__mro__)Output shape:
(MemoryStore, Store, ABC, object)The abstract method machinery uses metaclass behavior, but method lookup still follows the MRO.
45.28 Virtual Subclasses
ABCs can register virtual subclasses.
from abc import ABC
class Plugin:
pass
class PluginABC(ABC):
pass
PluginABC.register(Plugin)
print(issubclass(Plugin, PluginABC))
print(PluginABC in Plugin.__mro__)Output:
True
FalseVirtual subclassing affects issubclass and isinstance, but it does not insert the ABC into the concrete class’s MRO.
Therefore, virtual base methods are not found by ordinary attribute lookup.
45.29 MRO and Mixins
A mixin is a class designed to be combined with other classes.
class JsonMixin:
def to_json(self):
import json
return json.dumps(self.to_dict())A mixin usually expects the final class to provide some methods:
class User(JsonMixin):
def to_dict(self):
return {"name": "Ada"}Mixins should be small, cooperative, and explicit about expectations.
Good mixins:
define narrow behavior
avoid heavy __init__
call super when overriding cooperative methods
avoid owning unrelated state
document required methods45.30 Mixin Ordering
Mixin order matters.
class LoggingMixin:
def save(self):
print("log")
return super().save()
class Store:
def save(self):
print("store")
class Model(LoggingMixin, Store):
pass
Model().save()Output:
log
storeIf the order is reversed:
class Model(Store, LoggingMixin):
passthen Store.save may run first and stop the chain.
Base order should be chosen deliberately.
45.31 MRO and Frameworks
Frameworks often rely on MRO behavior.
Examples:
ORM model classes
class-based web views
serializer classes
test case classes
form classes
plugin base classes
dataclass-like transformsA class-based view might combine:
class View:
def dispatch(self):
...
class AuthMixin:
def dispatch(self):
check_auth()
return super().dispatch()
class LoggingMixin:
def dispatch(self):
log_request()
return super().dispatch()
class UserView(AuthMixin, LoggingMixin, View):
passThe MRO defines the request handling chain.
45.32 Inspecting the MRO
Use __mro__:
print(UserView.__mro__)Use mro():
print(UserView.mro())Use inspect.getmro:
import inspect
print(inspect.getmro(UserView))When debugging inheritance, always inspect the actual MRO rather than guessing from the class diagram.
45.33 Tracing Method Resolution
To see where a method comes from:
def where_defined(cls, name):
for base in cls.__mro__:
if name in base.__dict__:
return base
return NoneExample:
print(where_defined(UserView, "dispatch"))This reports the first class in the MRO that defines the attribute.
For descriptor-aware behavior, this only finds the raw defining class. Actual access may still involve descriptor binding.
45.34 MRO and __bases__
A class stores direct bases in __bases__.
class A:
pass
class B(A):
pass
print(B.__bases__)
print(B.__mro__)__bases__ gives immediate parents.
__mro__ gives the full linearized search order.
These are related but not the same.
45.35 Modifying __bases__
Python allows changing __bases__ in some cases.
class A:
pass
class B:
pass
class C(A):
pass
C.__bases__ = (B,)This asks CPython to recompute the MRO and validate layout compatibility.
It may fail if the new bases are incompatible.
Dynamic base changes are rare and should be avoided in normal code. They can invalidate assumptions about layout, methods, and type checks.
45.36 Layout Conflicts
Multiple inheritance is constrained by instance layout.
Some built-in types cannot be combined freely:
class Bad(dict, list):
passThis fails because dict and list have incompatible C-level memory layouts.
CPython must ensure that instances have a coherent object layout.
The MRO is about method order, but class creation must also validate memory layout and slot compatibility.
45.37 MRO and __slots__
Slots affect instance layout.
class A:
__slots__ = ("a",)
class B:
__slots__ = ("b",)
class C(A, B):
passMultiple inheritance with slots can fail depending on layout compatibility.
Only one base class with a non-empty instance layout can usually contribute certain low-level layout features.
This is a CPython-level constraint, not merely a method lookup rule.
45.38 MRO and Special Methods
Special methods are resolved on the type, usually through slots.
Example:
class A:
def __len__(self):
return 1
class B(A):
pass
print(len(B()))CPython finds the special method through class machinery.
But assigning __len__ to an instance does not affect len(obj):
b = B()
b.__len__ = lambda: 100
print(len(b))Still:
1Special method lookup depends on the class and its MRO, not ordinary instance attribute lookup.
45.39 MRO and Operator Overloading
Operators use special methods found through type lookup.
class A:
def __add__(self, other):
return "A add"
class B(A):
pass
print(B() + B())The implementation is found through the class hierarchy.
If a subclass overrides __add__, it wins:
class C(A):
def __add__(self, other):
return "C add"
print(C() + C())The MRO controls which special method is selected, subject to binary operator dispatch rules.
45.40 MRO and Binary Operators
Binary operator lookup has extra rules for subclasses.
For:
a + bCPython considers methods such as:
type(a).__add__
type(b).__radd__If the right operand’s type is a subclass of the left operand’s type, CPython may give the right operand’s reflected method priority.
This is still type-based lookup, but it is more complex than a simple MRO search on one class.
The key point: operator dispatch uses class-level special methods, not instance attributes.
45.41 MRO and super(type, obj)
The explicit form of super is:
super(CurrentClass, obj)This creates a proxy that starts searching after CurrentClass in type(obj).__mro__.
Example:
class A:
def f(self):
return "A"
class B(A):
def f(self):
return "B" + super(B, self).f()Inside B.f, this searches after B.
Zero-argument super() is compiler-assisted. CPython stores enough context to identify the current class and first argument.
45.42 Zero-Argument super
This form:
super()works inside normal methods because the compiler creates a hidden __class__ cell when needed.
Example:
class B(A):
def f(self):
return super().f()The call is roughly equivalent to:
super(__class__, self).f()The hidden __class__ cell is part of class creation and function closure handling.
This is one reason super() has special compiler support.
45.43 MRO and __class__ Cell
When a method references __class__ or uses zero-argument super(), the compiler creates a __class__ closure cell.
class A:
def f(self):
return __class__This cell is filled with the created class object after class creation.
Metaclasses that manipulate class namespaces must preserve this cell correctly, or class creation can fail.
This connects the compiler, class creation, closures, and MRO behavior.
45.44 Customizing MRO With Metaclasses
A metaclass can customize MRO computation by defining mro.
class Meta(type):
def mro(cls):
return super().mro()
class A(metaclass=Meta):
passThis is rare.
Changing MRO behavior can break assumptions in Python’s object model. It can affect descriptors, super, special methods, and framework behavior.
Most metaclasses should not override mro.
45.45 MRO Entries and Generic Aliases
Some objects in base-class position can replace themselves using __mro_entries__.
Example shape:
class Alias:
def __mro_entries__(self, bases):
return (RealBase,)
class C(Alias()):
passThe MRO is computed after base substitution.
This hook is important for typing-related constructs and generic aliases.
It allows syntax in base positions to behave differently from direct inheritance.
45.46 MRO and Generic Classes
Modern Python typing can make class bases look complex:
from typing import Generic, TypeVar
T = TypeVar("T")
class Box(Generic[T]):
passAt runtime, the resulting class still has an MRO.
print(Box.__mro__)Typing machinery may use __mro_entries__, metadata attributes, and special base classes, but ordinary method lookup still relies on the class MRO.
45.47 MRO and Protocols
Protocols from typing define structural interfaces for type checkers.
from typing import Protocol
class SizedLike(Protocol):
def __len__(self) -> int:
...A class does not need SizedLike in its MRO for a static type checker to consider it compatible.
Runtime method lookup remains nominal and MRO-based.
Static structural typing and runtime MRO lookup are different systems.
45.48 Practical MRO Debugging
When a method call surprises you, ask these questions:
What is type(obj)?
What is type(obj).__mro__?
Which class first defines the attribute?
Is the attribute a data descriptor?
Is an instance dictionary value shadowing it?
Does the method call super?
Does every class in the chain cooperate?
Is a class imported twice under different names?
Is a metaclass involved?Use code:
def explain_lookup(obj, name):
cls = type(obj)
print("type:", cls)
print("mro:", cls.__mro__)
for base in cls.__mro__:
if name in base.__dict__:
print("found in:", base)
print("raw value:", base.__dict__[name])
break
else:
print("not found in class MRO")
if hasattr(obj, "__dict__"):
print("instance dict:", obj.__dict__)This gives a concrete view of lookup.
45.49 Design Rules for Multiple Inheritance
Use multiple inheritance when the classes are designed to cooperate.
Prefer mixins that are narrow and stateless.
Keep base order deliberate.
Call super() in cooperative methods.
Use compatible method signatures.
Avoid unrelated concrete base classes with independent state.
Avoid mixing classes with incompatible C-level layouts.
Inspect __mro__ when behavior is unclear.
Do not use multiple inheritance as a substitute for composition when the relationship is not truly type-level behavior.
45.50 Key Points
The MRO is the linear class search order used by inheritance.
CPython stores the MRO on each class as __mro__.
Python uses C3 linearization for normal multiple inheritance.
The MRO preserves local base order and parent ordering constraints.
Attribute lookup, method lookup, descriptors, super(), and many special methods depend on the MRO.
super() continues after the current class in the runtime MRO.
Diamond inheritance works because shared ancestors appear once.
Some inheritance graphs are invalid because no consistent MRO exists.
Virtual ABC registration affects isinstance and issubclass, but it does not insert classes into the MRO.
Good multiple inheritance requires cooperative classes, compatible signatures, and deliberate base ordering.