Python MRO: Demystified for Junior Developers

By Intelluna Team
10 min read
Python MRO: Demystified for Junior Developers

Python MRO: Demystified for Junior Developers

TL;DR: MRO (Method Resolution Order) is the rule Python uses to decide which method to call when multiple classes are involved. You’ll meet it whenever you use inheritance, mixins, or super().


What is the MRO?

When you do obj.method() Python walks a list of classes to find method. That list is the MRO. In Python 3, it is computed using C3 linearization (fancy words for a consistent, left‑to‑right ordering that respects parent orders).

You can always see it:

class A: ...
class B(A): ...
class C(B): ...

print(C.mro())
# [<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>]

Why you should care

  • Avoid surprises when multiple parents define the same method name.
  • Use super() correctly so every class in the chain can run its part.
  • Design mixins that cooperate cleanly across projects.

Single Inheritance: Easy Mode

class Animal:
    def speak(self):
        print("Animal speaks")

class Dog(Animal):
    def speak(self):
        print("Woof!")

Dog().speak()     # Woof!
print(Dog.mro())  # Dog → Animal → object

Because Dog overrides speak, Python stops at Dog—no need to look at Animal.


Multiple Inheritance & the Diamond

Suppose two parents inherit from the same grandparent:

class Root:
    def ping(self):
        print("Root.ping")

class A(Root):
    def ping(self):
        print("A.ping")
        super().ping()  # delegate to next in MRO

class B(Root):
    def ping(self):
        print("B.ping")
        super().ping()

class C(A, B):
    pass

C().ping()
print(C.mro())

Output

A.ping
B.ping
Root.ping
[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Root'>, <class 'object'>]

What happened?

  • Python searched in left‑to‑right order of bases (A then B).
  • Because both A and B used super(), the call continued through the chain and reached Root once (no duplicates).

🧠 Tip: This “no duplicates, keeps parent order” behavior is exactly what C3 guarantees.


super() is your friend (when everyone cooperates)

If a class in the chain doesn’t call super(), it can break the flow:

class A:
    def ping(self):
        print("A.ping")
        # no super() here 😬

class B:
    def ping(self):
        print("B.ping")
        # no super() either

class C(A, B):
    pass

C().ping()  # Only A.ping runs
print(C.mro())  # [C, A, B, object]

Rule of thumb: In cooperative hierarchies (especially with mixins), always write methods like:

class SomeMixin:
    def process(self):
        # do mixin thing first
        print("SomeMixin.process")
        return super().process()

…and make sure every class in the chain calls super() (including the concrete/base class at the end, which should usually call super() or be the terminus if it owns the method).


Designing Mixins the Safe Way

Mixins are small classes that add behavior. Order matters!

class SaveCore:
    def save(self):
        print("SaveCore: writing to DB")

class LoggingMixin:
    def save(self):
        print("LoggingMixin: log before save")
        return super().save()

class TimingMixin:
    def save(self):
        print("TimingMixin: start timer")
        result = super().save()
        print("TimingMixin: stop timer")
        return result

class Service(LoggingMixin, TimingMixin, SaveCore):
    pass

Service().save()
print(Service.mro())

Output

LoggingMixin: log before save
TimingMixin: start timer
SaveCore: writing to DB
TimingMixin: stop timer
[Service, LoggingMixin, TimingMixin, SaveCore, object]

Guidelines

  • Put mixins to the left of the concrete/base class they extend.
  • Make all mixins and base classes use super() in the method they share.
  • Keep mixins single‑purpose and stateless when possible.

Quick Mental Model (C3 in plain English)

C3 linearization builds an MRO that:

  1. Lists the class before its parents.
  2. Respects the left‑to‑right order of bases.
  3. Preserves each parent’s own MRO order (no contradictory jumps).

You don’t need to compute C3 by hand—just remember: left‑to‑right, no repeats, keep parent order.


Debugging & Inspecting the MRO

  • MyClass.mro() → the exact resolution order.
  • import inspect; inspect.getmro(MyClass) → same idea.
  • help(MyClass) → shows the MRO in the docs view.

To see which implementation ran:

class X:
    def foo(self): print("X.foo")
class Y(X):
    def foo(self): print("Y.foo"); super().foo()
class Z(X):
    def foo(self): print("Z.foo"); super().foo()
class W(Y, Z):
    pass

W().foo()        # Y.foo → Z.foo → X.foo
print(W.mro())   # [W, Y, Z, X, object]

Common Pitfalls

  • Forgetting super() in one class of the chain → breaks cooperative behavior.
  • Ordering mixins incorrectly (e.g., putting the concrete base first).
  • Two unrelated parents implement the same method but are not designed to cooperate. Consider renaming or composing instead of inheriting.

Mini‑Exercises

  1. Predict the output. Change the order of bases in class C(B, A): (instead of class C(A, B):) and predict the new MRO and printout.

  2. Write a cooperative __init__. Create two mixins that both need to run in __init__ (e.g., AuthMixin, ConfigMixin). Ensure both execute once using super().

  3. Design a mixin trio. Build AuditMixin, CacheMixin, and ServiceCore and verify that all three run exactly once in the correct order.


Cheat Sheet

  • Show order: MyClass.mro()
  • Use super() everywhere in the shared method.
  • Put mixins before the concrete base.
  • Remember the mantra: left‑to‑right, no repeats, keep parent order.

Happy coding—and may your MROs always be predictable! 🚀

Enjoyed this article?

Stay updated with our latest insights on technology and innovation.