Skip to content

45. The MRO

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):
    pass

the 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.name

and 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:

A

because 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.attr

normal 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
object

It 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 method

The 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
object

This 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 MROs

Local precedence order means bases listed earlier in the class definition should remain earlier when possible.

class C(A, B):
    pass

means 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):
    pass

C3 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):
    pass

Inputs:

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 object

Result:

[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, object

A 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
object

B does not define f.

C defines f.

So the result is:

C

Even 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:

DBCA

The MRO is:

D, B, C, A, object

So 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):
    pass

This fails.

X requires:

A before B

Y requires:

B before A

Z cannot satisfy both.

CPython raises:

TypeError: Cannot create a consistent method resolution order

This 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):
    pass

states 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 property

The 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:

instance

The 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.attr

CPython 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 machinery

Metaclasses 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
False

Virtual 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 methods

45.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
store

If the order is reversed:

class Model(Store, LoggingMixin):
    pass

then 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 transforms

A 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):
    pass

The 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 None

Example:

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):
    pass

This 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):
    pass

Multiple 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:

1

Special 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 + b

CPython 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):
    pass

This 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()):
    pass

The 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]):
    pass

At 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.