Method Resolution Order

The Method Resolution Order is the precise, deterministic sequence Python searches through a class's ancestors when looking up an attribute -- essential for understanding multiple inheritance and super().

When a class inherits from several base classes, and more than one of them defines the same method, which one runs? Python answers this with the Method Resolution Order (MRO): a single, deterministic, linear ordering of a class and all its ancestors, computed by an algorithm called C3 linearization. Attribute lookup and super() both walk this exact order.

What Is the MRO

The MRO is the sequence of classes Python consults, in order, when resolving obj.attr or Class.attr: the class itself, then its bases, arranged so that each class appears before its own parents and, in multiple inheritance, in an order consistent with how the bases were listed. Every class -- not just ones using multiple inheritance -- has an MRO; for single inheritance it's just the simple chain up to object.

class Animal:
    pass


class Dog(Animal):
    pass


print(Dog.__mro__)
# (<class 'Dog'>, <class 'Animal'>, <class 'object'>)

Inspecting the MRO: __mro__ and mro()

Class.__mro__ is a tuple giving the computed order directly. Class.mro() is the method that computes it (called once, automatically, when the class is created); you can call it yourself to see the same result, and it's the method a metaclass could override to customize linearization.

There's also the built-in function inspect.getmro(D), which returns the same tuple as D.__mro__ -- useful when you have a class object but prefer a function-call style.

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

print(D.__mro__)
# (D, B, C, A, object)
print(D.mro())
# same list, as a list instead of a tuple

The Diamond Problem

The classic scenario the MRO exists to solve is the 'diamond': D inherits from both B and C, and both B and C inherit from a common ancestor A. A naive depth-first search (visit D, then B, then B's parent A, then C, then C's parent A again) would visit A twice and could even call an overridden method on A. The MRO instead guarantees each ancestor appears exactly once, and that a subclass is always searched before any of its own parents.

A
     / \
    B   C
     \ /
      D

D's MRO: D, B, C, A, object -- A appears once, after both B and C

C3 Linearization

Python computes the MRO with the C3 linearization algorithm (adopted in Python 2.3 and used ever since). Informally, C3 merges the MROs of the base classes together with the list of bases itself, always picking the next class that appears at the head of at least one list and does not appear anywhere later ('in the tail') in any other list. This guarantees three properties: each class appears exactly once, a subclass always precedes its own base classes, and the relative order that base classes were listed in is preserved.

You do not need to hand-compute C3 linearization for typical use; the important practical takeaways are the three guarantees above, and that Python raises TypeError: Cannot create a consistent method resolution order at class-definition time if a base-class ordering makes a consistent linearization impossible.

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

# C3 merges: L[D] = D + merge(L[B], L[C], [B, C])
#          = D + merge([B, A, object], [C, A, object], [B, C])
# step by step: take B (head of first list, not in any tail) -> D, B
#               take C (head of remaining lists, not in any tail) -> D, B, C
#               take A -> D, B, C, A
#               take object -> D, B, C, A, object
print(D.__mro__)  # (D, B, C, A, object)

When the MRO Cannot Be Computed

If the base classes are listed in an order that contradicts itself (e.g. one base implies X should come before Y, another implies Y before X), C3 linearization fails and Python refuses to create the class at all, rather than silently picking an arbitrary order.

class X: pass
class Y: pass
class A(X, Y): pass
class B(Y, X): pass

try:
    class C(A, B):  # A wants X before Y; B wants Y before X -- contradiction
        pass
except TypeError as e:
    print("caught:", e)

super() Follows the MRO, Not the Class Hierarchy Directly

This is the single most important practical consequence of the MRO: super() does not mean 'my direct parent class'. It means 'the next class after me in this instance's MRO'. Under multiple inheritance, that can be a sibling class, not an ancestor in the usual sense -- which is exactly what makes cooperative multiple inheritance work.

B.greet's super().greet() call resolves to whatever comes after B in D's MRO -- which is C, not A -- because super() is always relative to the full MRO of the actual instance's type, not to B's own static parent list.

class A:
    def greet(self):
        print("A.greet")


class B(A):
    def greet(self):
        print("B.greet")
        super().greet()


class C(A):
    def greet(self):
        print("C.greet")
        super().greet()


class D(B, C):
    def greet(self):
        print("D.greet")
        super().greet()


D().greet()
# D.greet
# B.greet
# C.greet   <- B's super() reaches C, a sibling, not A directly
# A.greet
print(D.__mro__)  # (D, B, C, A, object)

Cooperative Multiple Inheritance

The pattern shown above -- every class in a multiple-inheritance chain calling super().method() rather than a hardcoded base class name -- is called cooperative multiple inheritance. It's 'cooperative' because each class in the chain has to consistently forward the call along the MRO for the whole mechanism to work; a class that hardcodes A.greet(self) instead of super().greet() breaks the chain for anyone downstream.

Each mixin does its own work and then calls super().save() to hand off to the next class in the MRO, letting several independent behaviors compose in one call.

class LoggingMixin:
    def save(self):
        print("logging save")
        super().save()  # cooperatively forwards along the MRO


class ValidatingMixin:
    def save(self):
        print("validating before save")
        super().save()


class Base:
    def save(self):
        print("actually saving")


class Record(LoggingMixin, ValidatingMixin, Base):
    pass


Record().save()
# logging save
# validating before save
# actually saving

MRO for Single Inheritance and object

Even a plain class with no explicit base has an MRO of length 2: itself, then object. Every class's MRO ends in object, since object is the implicit root of every class hierarchy.

class Simple:
    pass

print(Simple.__mro__)  # (Simple, object)

Practical Usage: Mixins

The MRO is what makes the 'mixin' pattern reliable: small classes that each add one piece of behavior (logging, comparison, serialization) can be combined into a single class via multiple inheritance, relying on the MRO and cooperative super() calls to chain them predictably in the order they're listed.

class JSONMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)


class ReprMixin:
    def __repr__(self):
        return f"{type(self).__name__}({self.__dict__})"


class Config(JSONMixin, ReprMixin):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

Edge Cases

Reordering the bases in a class statement can change the MRO and therefore which method actually runs -- class D(B, C) and class D(C, B) generally produce different MROs. Also, super() with no arguments only works inside a method body defined in a class (it relies on the implicit __class__ closure cell); calling super() outside that context requires the explicit two-argument form super(ClassName, obj).

class D(B, C): pass   # MRO: D, B, C, A, object
class E(C, B): pass   # MRO: E, C, B, A, object -- different order, different lookup results

Key Takeaways

The MRO is the deterministic order Python searches a class and its ancestors when resolving an attribute, computed by C3 linearization.

C3 linearization guarantees each ancestor appears once, a subclass precedes its own bases, and listed base order is preserved.

If no consistent order exists, Python raises TypeError at class-definition time rather than guessing.

super() resolves to 'the next class in the instance's MRO', which under multiple inheritance can be a sibling class, not a direct parent.

Cooperative multiple inheritance -- every class forwarding via super() -- is what makes mixins compose predictably.

class B(A): def greet(self): A.greet(self) # hardcodes the base class class B(A): def greet(self): super().greet() # forwards along the MRO

Hardcoding the base class name bypasses the MRO and breaks cooperative multiple inheritance -- if B is later combined with a sibling class via multiple inheritance, the hardcoded call skips it entirely.

super() is relative to the actual runtime MRO of the instance's type, not to the textually nearest base class -- under multiple inheritance it can resolve to a sibling class.

class C(A, B): pass class D(B, A): pass class E(C, D): pass # TypeError: inconsistent MRO

Contradictory base-class orderings across a hierarchy make C3 linearization impossible; Python raises TypeError at class definition rather than picking an arbitrary order.

What three guarantees does C3 linearization make about the MRO it produces?

Each ancestor class appears exactly once; a subclass always appears before its own base classes; and the relative order in which base classes were listed is preserved.

In class D(B, C), where both B and C inherit from A, why doesn't Python's MRO visit A twice?

Because C3 linearization explicitly merges the ancestor lists so each class appears only once in the final order -- it is designed to eliminate the double-visit that a naive depth-first search of a diamond hierarchy would produce.

What does super() actually resolve to -- 'my class's direct parent' or something else?

The next class after the current one in the actual instance's Method Resolution Order -- under multiple inheritance this can be a sibling class rather than a direct ancestor of the class where super() is called.

What happens if Python cannot compute a consistent MRO for a class definition?

It raises TypeError ('Cannot create a consistent method resolution order') at class-definition time, refusing to create the class rather than silently choosing an arbitrary order.

How would swapping the order of base classes in class D(B, C) to class D(C, B) affect method lookup?

It generally changes the MRO (and therefore which class's method is found first for any name both B and C define), since C3 linearization respects the listed order of bases as one of its inputs.

What is 'cooperative multiple inheritance', and why is calling super().method() in every class necessary to achieve it?

It's a pattern where every class in a multiple-inheritance chain calls super().method() instead of hardcoding a specific base class, so calls forward along the whole MRO; skipping super() in any one class breaks the chain for classes further down the MRO.

What is the MRO of a class with no explicit base classes, e.g. class Simple: pass?

(Simple, object) -- every class implicitly inherits from object, so even a base-less class has a two-element MRO.

What does this print?

'B' then (D, B, C, A, object) -- D defines no who() itself, so lookup follows the MRO and finds B.who() first, since B precedes C in D's linearized MRO.

Write three mixin classes (TimestampMixin, ValidationMixin, Base) that each define a save() method calling super().save(), print something distinguishing before doing so, and combine them into a Record class demonstrating cooperative multiple inheritance. Print Record().save()'s output order.

class TimestampMixin: def save(self): print("stamping timestamp") super().save() class ValidationMixin: def save(self): print("validating") super().save() class Base: def save(self): print("writing to storage") class Record(TimestampMixin, ValidationMixin, Base): pass Record().save() # stamping timestamp # validating # writing to storage

Each mixin's save() prints its own message and then calls super().save(), which the MRO (Record, TimestampMixin, ValidationMixin, Base, object) routes to the next class in sequence, chaining all three behaviors in the order the bases were listed.

What does this print, and why?

'error' -- A implies X must precede Y, while B implies Y must precede X; C3 linearization cannot satisfy both constraints simultaneously, so Python raises TypeError when defining class C, which the except clause catches.

class A:
    def who(self): return "A"

class B(A):
    def who(self): return "B"

class C(A):
    def who(self): return "C"

class D(B, C):
    pass

print(D().who())
print(D.__mro__)
class X: pass
class Y: pass
class A(X, Y): pass
class B(Y, X): pass

try:
    class C(A, B):
        pass
except TypeError as e:
    print("error")