MRO - Method Resolution Order and the C3 Linearisation Algorithm
Reading time: ~30 minutes | Level: Intermediate → Engineering
Before reading further, predict the output of this program:
class A:
def hello(self):
print("A")
class B(A):
def hello(self):
print("B")
super().hello()
class C(A):
def hello(self):
print("C")
super().hello()
class D(B, C):
def hello(self):
print("D")
super().hello()
D().hello()
Most developers predict D → B → A or D → B → C → A. The actual output is:
D
B
C
A
B.hello() calls super().hello() and gets C, not A. If you did not predict this exactly, your mental model of super() is incorrect - and that means every mixin you write could have subtle bugs. This lesson gives you the precise model.
What You Will Learn
- What MRO is and why Python needs it
- The diamond problem and why it required a new algorithm
- C3 linearisation: the algorithm Python uses, stated precisely with worked examples
- How to read
__mro__andmro()and what they tell you - How
super()traverses the MRO - it is not "call the parent class" - Mixin patterns that depend on MRO to work correctly
- Real Django and Flask mixin examples with MRO walkthrough
- MRO failure cases - when Python raises
TypeError: Cannot create a consistent method resolution order - Practical rules for designing safe multiple inheritance hierarchies
Prerequisites
- Lessons 01–07 of this module
- Basic familiarity with Python inheritance syntax
- Understanding that
super()exists and is used in inheritance chains
Part 1 - What Is MRO and Why Does It Exist?
Single Inheritance: Trivial
In single inheritance, method resolution is straightforward - search the class, then its parent, then its parent's parent, and so on up to object.
class A:
def greet(self): print("A")
class B(A):
pass
class C(B):
pass
C().greet() # Searches C → B → A → finds it in A → prints "A"
print(C.__mro__)
# (<class 'C'>, <class 'B'>, <class 'A'>, <class 'object'>)
No ambiguity. No algorithm needed beyond "go up the chain".
Multiple Inheritance: The Diamond Problem
When a class inherits from two parents that share a common ancestor, the resolution becomes ambiguous:
class Base:
def method(self):
print("Base.method")
class Left(Base):
def method(self):
print("Left.method")
super().method()
class Right(Base):
def method(self):
print("Right.method")
super().method()
class Child(Left, Right):
pass
Base
/ \
Left Right
\ /
Child
When Child().method() is called, which method should run? Left.method or Right.method? And if both run, should Base.method run once or twice?
This is the diamond problem. Python's answer is MRO via the C3 linearisation algorithm, which guarantees:
- Consistent, predictable resolution order
Base.methodis called exactly once- The resolution respects the order in which bases are declared
Part 2 - The C3 Linearisation Algorithm
The Formula
C3 linearisation computes the MRO of class C with bases B1, B2, ..., Bn as:
L[C] = C + merge(L[B1], L[B2], ..., L[Bn], [B1, B2, ..., Bn])
Where merge is applied iteratively: at each step, take the head (first element) of the first sequence if that head does not appear in the tail (all elements except the first) of any other sequence. Add it to the result and remove it from all sequences. Repeat until all sequences are empty. If no valid head exists, raise TypeError.
Worked Example 1 - Linear Inheritance
class A:
pass
class B(A):
pass
class C(B):
pass
L[object] = [object]
L[A] = A + merge(L[object], [object])
= A + merge([object], [object])
= A, object
= [A, object]
L[B] = B + merge(L[A], [A])
= B + merge([A, object], [A])
# Head of first list: A. Is A in tail of [A]? No tail. Take A.
= B, A + merge([object], [])
# Head: object. Take it.
= [B, A, object]
L[C] = C + merge(L[B], [B])
= C + merge([B, A, object], [B])
# Head: B. Is B in tail of [B]? No. Take B.
= C, B + merge([A, object], [])
= [C, B, A, object]
print(C.__mro__)
# (<class 'C'>, <class 'B'>, <class 'A'>, <class 'object'>)
Worked Example 2 - The Diamond
class O: # think of as object
pass
class A(O):
pass
class B(O):
pass
class C(A, B):
pass
L[O] = [O]
L[A] = [A, O]
L[B] = [B, O]
L[C] = C + merge(L[A], L[B], [A, B])
= C + merge([A, O], [B, O], [A, B])
Step 1: Head of first list = A.
Is A in tail of [B, O]? No.
Is A in tail of [A, B]? No (tail is [B]).
Take A. Remove A from all lists.
Result so far: [C, A]
Remaining: merge([O], [B, O], [B])
Step 2: Head of first list = O.
Is O in tail of [B, O]? YES - O appears in tail.
Skip. Try next list.
Head of second list = B.
Is B in tail of [O]? No.
Is B in tail of [B]? No (tail is []).
Take B. Remove B from all lists.
Result so far: [C, A, B]
Remaining: merge([O], [O], [])
Step 3: Head = O. Is O in tail of [O]? No. Take O.
Result: [C, A, B, O]
print(C.__mro__)
# (<class 'C'>, <class 'A'>, <class 'B'>, <class 'O'>)
Worked Example 3 - Full Diamond with object
class Base:
pass
class Left(Base):
pass
class Right(Base):
pass
class Child(Left, Right):
pass
print(Child.__mro__)
# (<class 'Child'>, <class 'Left'>, <class 'Right'>, <class 'Base'>, <class 'object'>)
Walk through the algorithm:
L[Base] = [Base, object]L[Left] = [Left, Base, object]L[Right] = [Right, Base, object]L[Child] = Child + merge([Left, Base, object], [Right, Base, object], [Left, Right])
Step 1: Head = Left. Not in any tail. Take Left.
Step 2: Head = Base. In tail of [Right, Base, object] - skip. Head of next = Right. Not in any tail. Take Right.
Step 3: Head = Base. Not in any tail now. Take Base.
Step 4: Take object.
Result: [Child, Left, Right, Base, object]
This is why Base.method is called exactly once - it appears once in the linearisation.
Part 3 - Reading __mro__ and mro()
Python exposes the computed MRO on every class.
class Plugin:
def activate(self): print("Plugin.activate")
class LoggingPlugin(Plugin):
def activate(self):
print("LoggingPlugin.activate")
super().activate()
class SecurityPlugin(Plugin):
def activate(self):
print("SecurityPlugin.activate")
super().activate()
class CombinedPlugin(LoggingPlugin, SecurityPlugin):
def activate(self):
print("CombinedPlugin.activate")
super().activate()
# Inspect the MRO
print(CombinedPlugin.__mro__)
# (<class 'CombinedPlugin'>, <class 'LoggingPlugin'>, <class 'SecurityPlugin'>,
# <class 'Plugin'>, <class 'object'>)
# mro() returns a list instead of a tuple
print(CombinedPlugin.mro())
# [<class 'CombinedPlugin'>, <class 'LoggingPlugin'>, <class 'SecurityPlugin'>,
# <class 'Plugin'>, <class 'object'>]
CombinedPlugin().activate()
# CombinedPlugin.activate
# LoggingPlugin.activate
# SecurityPlugin.activate
# Plugin.activate
Each super().activate() call does not go to the class's own parent - it goes to the next class in the MRO of the instance's actual type.
Use __mro__ to debug resolution order before it surprises you. Make it a habit when designing mixin chains.
Always check .__mro__ when debugging unexpected method resolution in multiple inheritance chains. Run print([cls.__name__ for cls in YourClass.__mro__]) to see the exact lookup order before you spend time reading code that may never be reached.
Part 4 - How super() Actually Works
The Critical Mental Model Shift
super() does not mean "call my parent class's method". It means "call the next class in the MRO of the instance's actual type, starting after the current class".
class A:
def method(self):
print("A.method")
class B(A):
def method(self):
print("B.method")
super().method() # next after B in the MRO of whatever instance this is
class C(A):
def method(self):
print("C.method")
super().method() # next after C in the MRO
class D(B, C):
def method(self):
print("D.method")
super().method()
# D.__mro__ = (D, B, C, A, object)
d = D()
d.method()
# D.method - D.method runs, super() finds next after D: B
# B.method - B.method runs, super() finds next after B: C
# C.method - C.method runs, super() finds next after C: A
# A.method - A.method runs, no super() call
When B.method executes super().method(), Python does not look at B's parent (A). It looks at the MRO of d (which is D) and finds the next class after B, which is C. B does not even know C exists.
super() With Explicit Arguments
super() in Python 3 uses the __class__ cell variable (injected at compile time) and self. Explicitly: super(CurrentClass, self).
class Base:
def method(self):
print("Base")
class Middle(Base):
def method(self):
print("Middle")
# These are equivalent in Python 3:
super().method()
super(Middle, self).method()
class Top(Middle):
def method(self):
print("Top")
super().method()
Top().method()
# Top
# Middle
# Base
The two-argument form is useful when you need to skip classes in the MRO - use it sparingly and only when you know precisely what you are doing.
super() without arguments only works in Python 3. In Python 2, you had to write super(ClassName, self).method() explicitly. If you ever need to maintain code that runs on both Python 2 and Python 3 (legacy projects), always use the explicit two-argument form. Python 3's zero-argument super() relies on a __class__ cell variable injected at compile time - it is not available in Python 2.
Why Every Mixin Must Call super()
If any class in the MRO chain does not call super(), the chain breaks and all classes above it are silently skipped.
class A:
def setup(self):
print("A.setup")
class B(A):
def setup(self):
print("B.setup")
# Missing super() call - A.setup will never run in a diamond
class C(A):
def setup(self):
print("C.setup")
super().setup()
class D(B, C):
def setup(self):
print("D.setup")
super().setup()
# D.__mro__ = (D, B, C, A, object)
D().setup()
# D.setup
# B.setup - B does NOT call super()
# C.setup and A.setup are SILENTLY SKIPPED
This is the most common mixin bug in production code. The fix: every class in a cooperative multiple inheritance chain must call super().
Part 5 - Mixin Patterns That Depend on MRO
The Cooperative Mixin Stack
class Base:
def process(self, data: dict) -> dict:
return data # terminal - returns without calling super()
class ValidationMixin:
def process(self, data: dict) -> dict:
if "id" not in data:
raise ValueError("Missing required field: id")
return super().process(data) # passes to next in MRO
class LoggingMixin:
def process(self, data: dict) -> dict:
print(f"Processing: {data}")
result = super().process(data)
print(f"Result: {result}")
return result
class NormalisationMixin:
def process(self, data: dict) -> dict:
normalised = {k.lower(): v for k, v in data.items()}
return super().process(normalised)
class DataProcessor(LoggingMixin, ValidationMixin, NormalisationMixin, Base):
pass
# MRO: DataProcessor → LoggingMixin → ValidationMixin → NormalisationMixin → Base
processor = DataProcessor()
result = processor.process({"ID": 42, "Name": "Alice"})
# Processing: {'ID': 42, 'Name': 'Alice'}
# Result: {'id': 42, 'name': 'alice'}
The MRO determines the order in which mixins apply. The declaration order in class DataProcessor(LoggingMixin, ValidationMixin, NormalisationMixin, Base) directly controls the MRO: leftmost first.
Parameterised Mixin Ordering
Because MRO is determined at class definition time, you can create different pipelines by reordering mixins:
# Validation before normalisation
class StrictProcessor(ValidationMixin, NormalisationMixin, Base):
pass
# Normalisation before validation (normalise first, then validate)
class PermissiveProcessor(NormalisationMixin, ValidationMixin, Base):
pass
# StrictProcessor MRO: StrictProcessor → Validation → Normalisation → Base
# PermissiveProcessor MRO: PermissiveProcessor → Normalisation → Validation → Base
Part 6 - Django and Flask Mixin Examples
Django CBV Mixin Stack
Django's class-based views are designed around cooperative multiple inheritance. The MRO is carefully engineered.
# Simplified Django mixin chain (not full Django code - illustrative)
class View:
def dispatch(self, request, *args, **kwargs):
method = request.method.lower()
handler = getattr(self, method, self.http_method_not_allowed)
return handler(request, *args, **kwargs)
class LoginRequiredMixin:
"""Redirect to login if not authenticated."""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect("/login/")
return super().dispatch(request, *args, **kwargs) # must call super()
class PermissionRequiredMixin:
"""Check permissions after authentication."""
permission_required = None
def dispatch(self, request, *args, **kwargs):
if not request.user.has_perm(self.permission_required):
return redirect("/forbidden/")
return super().dispatch(request, *args, **kwargs) # must call super()
class MyView(LoginRequiredMixin, PermissionRequiredMixin, View):
permission_required = "myapp.view_report"
def get(self, request):
return HttpResponse("Report")
# MyView MRO:
# MyView → LoginRequiredMixin → PermissionRequiredMixin → View → object
#
# dispatch() chain:
# LoginRequiredMixin.dispatch checks auth → calls super()
# PermissionRequiredMixin.dispatch checks permission → calls super()
# View.dispatch routes to self.get() → returns response
The ordering rule in Django: Authentication mixins go leftmost (checked first). View always goes rightmost (the terminal). If you reversed LoginRequiredMixin and PermissionRequiredMixin, you would check permissions before checking if the user is even authenticated - a security ordering bug caused by MRO misunderstanding.
Flask MethodView and Mixin Composition
from flask.views import MethodView
class AuthMixin:
def dispatch_request(self, *args, **kwargs):
# Flask equivalent: check auth before routing
token = request.headers.get("Authorization")
if not token:
return {"error": "Unauthorized"}, 401
return super().dispatch_request(*args, **kwargs)
class RateLimitMixin:
def dispatch_request(self, *args, **kwargs):
# Check rate limit
if self._is_rate_limited():
return {"error": "Too Many Requests"}, 429
return super().dispatch_request(*args, **kwargs)
def _is_rate_limited(self) -> bool:
return False # implementation omitted
class UserAPI(AuthMixin, RateLimitMixin, MethodView):
def get(self, user_id: int):
return {"user_id": user_id}
# MRO: UserAPI → AuthMixin → RateLimitMixin → MethodView → View → object
Part 7 - MRO Failure Cases
When C3 Cannot Produce a Consistent Order
C3 raises TypeError when no consistent linearisation exists - when the inheritance order declared by the programmer directly contradicts the monotonicity constraint.
class A:
pass
class B(A):
pass
# Attempt to put A before B, but B already requires B before A
class C(A, B): # TypeError!
pass
# TypeError: Cannot create a consistent method resolution order (MRO)
# for bases A, B
Why? C(A, B) declares that A comes before B. But B(A) means B must come before A in B's own MRO. These two constraints cannot both be satisfied.
# More subtle violation
class X:
pass
class Y(X):
pass
class Z(X):
pass
class Problem(Y, X, Z): # TypeError!
pass
# Z(X) requires X after Z, but the declaration Y, X, Z puts X before Z
If you get TypeError: Cannot create a consistent method resolution order (MRO), it means your class hierarchy directly violates the C3 monotonicity constraint - two bases disagree on the order in which a shared ancestor should appear. This is not a Python bug and cannot be fixed by reordering imports or changing runtime state. You must restructure the inheritance hierarchy itself, typically by removing or reordering base classes.
Reading the Error
TypeError: Cannot create a consistent method resolution order (MRO)
for bases X, Z
Python tells you which bases conflict. The fix is to resolve the contradiction in your inheritance declarations, usually by reordering or restructuring the hierarchy.
The Inheritance Order Rule
A safe rule: declare more specific (derived) classes before less specific (base) classes.
# Correct: more derived classes listed before their parents
class GoodOrder(LoggingMixin, ValidationMixin, Base):
pass
# Bad: Base listed before derived mixin - likely to cause issues with further subclassing
class BadOrder(Base, LoggingMixin):
pass
Part 8 - Practical Rules for Safe Multiple Inheritance
Rule 1: Always Call super() in Every Method Participating in a Chain
If any class in the chain breaks the cooperative calling convention, all classes above it in the MRO are silently bypassed.
class SafeMixin:
def method(self, *args, **kwargs):
# Do your work
result = self._do_work()
# Always delegate upward
return super().method(*args, **kwargs) # never skip this
Rule 2: Design for MRO - Specify Terminal Classes
Every cooperative chain needs a terminal class that does NOT call super() (or calls it to object which has no-op versions of most hooks).
class TerminalBase:
"""Terminal base: does not call super() further."""
def setup(self):
pass # intentional no-op - end of chain
class MixinOne(TerminalBase):
def setup(self):
print("MixinOne setup")
super().setup()
class MixinTwo(TerminalBase):
def setup(self):
print("MixinTwo setup")
super().setup()
class App(MixinOne, MixinTwo, TerminalBase):
pass
App().setup()
# MixinOne setup
# MixinTwo setup
Rule 3: Use __mro__ to Verify Before Deploying
After defining a multi-inheritance class, print its MRO and walk through it manually:
class Composed(MixinA, MixinB, MixinC, Base):
pass
print([cls.__name__ for cls in Composed.__mro__])
# ['Composed', 'MixinA', 'MixinB', 'MixinC', 'Base', 'object']
Does this order make sense for your use case? Is every required setup performed before it is needed? Does the terminal base come last?
Rule 4: Keep Mixin Hierarchies Shallow
Mixins should not themselves inherit from multiple classes. Keep the mixin inheritance graph flat. Each mixin inherits from one thing (usually object implicitly, or a single base class).
# Problematic: mixin with its own multiple inheritance
class ComplexMixin(MixinA, MixinB): # creates a complex sub-graph
pass
# Preferred: flat mixins
class SimpleMixin:
def feature(self): ...
Rule 5: Document MRO Assumptions
When writing a mixin that requires certain methods to exist on self, document that requirement explicitly:
class SerializationMixin:
"""
Mixin that adds JSON serialisation.
Requires:
- self.to_dict() -> dict (must be implemented by the concrete class)
MRO position: should come before any class that calls super().to_json(),
and after classes that modify self.to_dict() output.
"""
def to_json(self) -> str:
import json
return json.dumps(self.to_dict()) # assumes to_dict() exists
Common Mistakes
Mistake 1 - Assuming super() Calls the Parent Class
class B(A):
def method(self):
super().method() # Does NOT always call A.method
# Calls the NEXT class in the MRO of the actual instance's type
# If an instance of C(B, A) calls this, super() might resolve to something else
Mistake 2 - Forgetting That MRO Is Determined at Class Creation Time
class D(B, C):
pass
# D's MRO is fixed the moment this line executes.
# You cannot change it at runtime.
# If you reorder B and C, you get a different MRO - it must be correct at design time.
Mistake 3 - Relying on MRO Without Printing It
Debug first. Print ClassName.__mro__ before deploying any multi-inheritance chain. What you think the order is and what C3 computes can diverge.
Mistake 4 - Not Passing Arguments Through in Cooperative super() Chains
# Wrong: drops arguments - everything after this receives no args
class MixinBad:
def __init__(self, **kwargs):
super().__init__() # kwargs not forwarded - broken chain
# Right: forward all unused arguments
class MixinGood:
def __init__(self, **kwargs):
super().__init__(**kwargs) # forward remaining kwargs to next in chain
Engineering Checklist
Before moving to the next lesson, verify you can answer these without looking:
- What is the diamond problem, and what does C3 linearisation solve?
- State the C3 merge rule in your own words.
- Given
class D(B, C)whereB(A)andC(A)- what is the MRO? Walk through C3 manually. - What does
super()actually do - specifically, what does "next in the MRO" mean? - Why must every mixin that overrides a method call
super()? - What happens when C3 cannot produce a consistent order? What error do you get?
- How do you inspect the MRO at runtime?
- In Django CBVs, why does
LoginRequiredMixingo leftmost? - What is the "cooperative calling convention" and why does it require that every class in the chain participate?
Key Takeaways
- MRO solves the diamond problem: when multiple inheritance paths converge on a shared ancestor, C3 linearisation ensures every class appears exactly once in the resolution order and that
Base.methodis called once, not twice. - C3 merge rule in plain English: at each step, take the head of the first list only if that head does not appear in the tail of any other list. If no head qualifies, raise
TypeError. super()does not mean "call my parent's method". It means "call the next class in the MRO of the instance's actual type, starting after the current class".Bdoes not know thatCwill come next - the MRO of the instantiated type determines this.- Every class in a cooperative multiple inheritance chain must call
super(). A single missingsuper()call silently bypasses every class above it in the MRO. super()without arguments only works in Python 3. Python 2 required the explicit formsuper(ClassName, self).- Always print
ClassName.__mro__before deploying any multi-inheritance class. What you think the order is and what C3 computes can diverge. TypeError: Cannot create a consistent method resolution ordermeans your hierarchy violates C3 monotonicity. Fix the inheritance declarations - you cannot patch it at runtime.- In Django CBVs, mixin ordering is security-critical: authentication mixins go leftmost (checked first),
Viewgoes rightmost (terminal). MRO controls this order. - Pass all
**kwargsthrough in cooperativesuper().__init__(**kwargs)calls or arguments will be silently dropped for every class further up the chain.
Graded Practice
Level 1 - Predict the Output
Question 1
class A:
def greet(self):
print("A")
class B(A):
def greet(self):
print("B")
super().greet()
class C(A):
def greet(self):
print("C")
super().greet()
class D(B, C):
pass
D().greet()
Show Answer
B
C
A
D does not define greet, so the MRO is consulted: D → B → C → A → object. B.greet runs first (leftmost class defining greet), calls super().greet(), which finds C next in the MRO - not A. C.greet runs, calls super().greet(), which finds A. A.greet runs with no further super() call. This is the canonical diamond resolution: all four classes participate, A runs once.
Question 2
class A:
def setup(self):
print("A.setup")
class B(A):
def setup(self):
print("B.setup")
# No super() call here
class C(A):
def setup(self):
print("C.setup")
super().setup()
class D(B, C):
def setup(self):
print("D.setup")
super().setup()
D().setup()
Show Answer
D.setup
B.setup
D.__mro__ is (D, B, C, A, object). D.setup calls super().setup() → B.setup runs and prints "B.setup". B.setup does NOT call super(), so the chain breaks there. C.setup and A.setup are silently skipped. No exception is raised - this is the most dangerous mixin bug because it fails invisibly. Always call super() in every method that participates in a cooperative chain.
Question 3
class X:
def run(self):
print("X")
class Y(X):
def run(self):
print("Y")
super().run()
class Z(X):
def run(self):
print("Z")
super().run()
class W(Y, Z):
def run(self):
print("W")
super().run()
print([c.__name__ for c in W.__mro__])
W().run()
Show Answer
['W', 'Y', 'Z', 'X', 'object']
W
Y
Z
X
C3 on W(Y, Z) where Y(X) and Z(X):
L[X] = [X, object]L[Y] = [Y, X, object]L[Z] = [Z, X, object]L[W] = W + merge([Y, X, object], [Z, X, object], [Y, Z])- Y is not in any tail - take Y
- X is in tail of
[Z, X, object]- skip; Z is not in any tail - take Z - X is now not in any tail - take X; take object
- Result:
[W, Y, Z, X, object]
Every class calls super(), so all four run() methods execute in MRO order.
Question 4
class Base:
def __init__(self):
print("Base.__init__")
class MixinA:
def __init__(self, **kwargs):
print("MixinA.__init__")
super().__init__(**kwargs)
class MixinB:
def __init__(self, **kwargs):
print("MixinB.__init__")
super().__init__(**kwargs)
class MyClass(MixinA, MixinB, Base):
def __init__(self):
print("MyClass.__init__")
super().__init__()
MyClass()
Show Answer
MyClass.__init__
MixinA.__init__
MixinB.__init__
Base.__init__
MyClass.__mro__ is (MyClass, MixinA, MixinB, Base, object). Each __init__ calls super().__init__() forwarding **kwargs, so the full chain executes in MRO order. Base.__init__ takes no extra kwargs and is the terminal. This is the correct pattern for cooperative mixin initialisation.
Question 5
class A:
pass
class B(A):
pass
try:
class C(A, B):
pass
print("C created successfully")
except TypeError as e:
print(f"TypeError: {e}")
Show Answer
TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B
C(A, B) declares that A must come before B in the MRO. But B inherits from A, meaning B must come before A in any consistent linearisation. These two requirements are contradictory - C3 cannot satisfy both simultaneously and raises TypeError immediately when the class body is executed. The fix: reverse the order to class C(B, A), which is consistent with B's own hierarchy.
Level 2 - Debug Challenge
The following mixin-based class is supposed to initialise all three mixins in order, but one mixin is silently skipped during __init__. Find the bug and fix it.
class TimestampMixin:
def __init__(self, **kwargs):
from datetime import datetime
self.created_at = datetime.utcnow()
# BUG IS HERE
class AuditMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.audit_log = []
class ValidatedMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.is_valid = True
class Record(TimestampMixin, AuditMixin, ValidatedMixin):
def __init__(self, name: str):
self.name = name
super().__init__()
r = Record("test")
print(hasattr(r, "created_at")) # True
print(hasattr(r, "audit_log")) # ???
print(hasattr(r, "is_valid")) # ???
Show Answer
The bug: TimestampMixin.__init__ does not call super().__init__(**kwargs). The MRO for Record is (Record, TimestampMixin, AuditMixin, ValidatedMixin, object). When TimestampMixin.__init__ runs and does not call super(), the chain stops there. AuditMixin.__init__ and ValidatedMixin.__init__ are never called.
Actual output:
True
False # audit_log never set
False # is_valid never set
Fixed TimestampMixin:
class TimestampMixin:
def __init__(self, **kwargs):
from datetime import datetime
super().__init__(**kwargs) # FIXED: forward to next in MRO
self.created_at = datetime.utcnow()
class AuditMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.audit_log = []
class ValidatedMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.is_valid = True
class Record(TimestampMixin, AuditMixin, ValidatedMixin):
def __init__(self, name: str):
self.name = name
super().__init__()
r = Record("test")
print(hasattr(r, "created_at")) # True
print(hasattr(r, "audit_log")) # True
print(hasattr(r, "is_valid")) # True
Every mixin in a cooperative chain must call super().__init__(**kwargs). The rule has no exceptions.
Level 3 - Design Challenge
Design a cooperative mixin system for a web request handler. A RequestHandler should support three independently composable behaviours:
AuthMixin- checks for a valid token before processing; callssuper()only if authenticatedLoggingMixin- logs every request before and after processingCachingMixin- returns a cached response if available; callssuper()only on cache miss
The handler class itself just returns {"data": "response"}. Design the MRO so that the call order is: Logging → Auth → Caching → Handler. Verify by printing the MRO and tracing the output when a cached response exists vs when it does not.
Show Answer
class BaseHandler:
"""Terminal handler - no super() call."""
def handle(self, request: dict) -> dict:
print("[BaseHandler] Processing request")
return {"data": "response"}
class CachingMixin:
"""Returns cached response on hit; calls super() on miss."""
def __init__(self):
self._cache: dict = {}
super().__init__()
def handle(self, request: dict) -> dict:
key = str(request)
if key in self._cache:
print(f"[CachingMixin] Cache HIT for {key}")
return self._cache[key]
print(f"[CachingMixin] Cache MISS - calling super()")
result = super().handle(request)
self._cache[key] = result
return result
class AuthMixin:
"""Checks token; calls super() only if authenticated."""
def handle(self, request: dict) -> dict:
token = request.get("token")
if not token:
print("[AuthMixin] No token - rejecting")
return {"error": "Unauthorized"}
print(f"[AuthMixin] Token valid - calling super()")
return super().handle(request)
class LoggingMixin:
"""Logs before and after; always calls super()."""
def handle(self, request: dict) -> dict:
print(f"[LoggingMixin] Request: {request}")
result = super().handle(request)
print(f"[LoggingMixin] Response: {result}")
return result
# MRO target: Handler → Logging → Auth → Caching → BaseHandler
class RequestHandler(LoggingMixin, AuthMixin, CachingMixin, BaseHandler):
pass
# Verify MRO
print([c.__name__ for c in RequestHandler.__mro__])
# ['RequestHandler', 'LoggingMixin', 'AuthMixin', 'CachingMixin', 'BaseHandler', 'object']
handler = RequestHandler()
print("\n--- First request (cache miss, authenticated) ---")
r1 = handler.handle({"token": "abc123", "path": "/api/data"})
print("\n--- Second request (cache hit) ---")
r2 = handler.handle({"token": "abc123", "path": "/api/data"})
print("\n--- Unauthenticated request ---")
r3 = handler.handle({"path": "/api/data"}) # no token
Output:
['RequestHandler', 'LoggingMixin', 'AuthMixin', 'CachingMixin', 'BaseHandler', 'object']
--- First request (cache miss, authenticated) ---
[LoggingMixin] Request: {'token': 'abc123', 'path': '/api/data'}
[AuthMixin] Token valid - calling super()
[CachingMixin] Cache MISS - calling super()
[BaseHandler] Processing request
[LoggingMixin] Response: {'data': 'response'}
--- Second request (cache hit) ---
[LoggingMixin] Request: {'token': 'abc123', 'path': '/api/data'}
[AuthMixin] Token valid - calling super()
[CachingMixin] Cache HIT for {'token': 'abc123', 'path': '/api/data'}
[LoggingMixin] Response: {'data': 'response'}
--- Unauthenticated request ---
[LoggingMixin] Request: {'path': '/api/data'}
[AuthMixin] No token - rejecting
[LoggingMixin] Response: {'error': 'Unauthorized'}
Key observations:
LoggingMixinalways runs (it always callssuper())AuthMixinshort-circuits the chain if no token (does not callsuper())CachingMixinshort-circuits the chain on a cache hit (does not callsuper())BaseHandleris only reached on an authenticated cache miss- The MRO declaration order directly controls the processing pipeline
What's Next
Lesson 09 covers Abstract Base Classes - the formal mechanism for declaring interfaces in Python. ABCs use ABCMeta to enforce that required methods are implemented at class creation time, rather than at runtime when you call a missing method. You will see how ABCs, collections.abc, and typing.Protocol each solve the interface problem from different angles.
