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 (
AthenB). - Because both
AandBusedsuper(), the call continued through the chain and reachedRootonce (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:
- Lists the class before its parents.
- Respects the left‑to‑right order of bases.
- 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
-
Predict the output. Change the order of bases in
class C(B, A):(instead ofclass C(A, B):) and predict the new MRO and printout. -
Write a cooperative
__init__. Create two mixins that both need to run in__init__(e.g.,AuthMixin,ConfigMixin). Ensure both execute once usingsuper(). -
Design a mixin trio. Build
AuditMixin,CacheMixin, andServiceCoreand 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! 🚀