Timothy stared at his screen in disbelief. "Margaret, I don't understand what's happening. I have a simple inheritance hierarchy - a LoggingMixin and a ValidationMixin that both inherit from BaseMixin, and my User class inherits from both. But when I call super().__init__(), methods are being called in a completely bizarre order. Sometimes the same method gets called twice, sometimes it skips methods entirely. What is going on?"
He showed her his code:
class BaseMixin:
def __init__(self):
print("BaseMixin.__init__")
super().__init__()
class LoggingMixin(BaseMixin):
def __init__(self):
print("LoggingMixin.__init__")
super().__init__()
class ValidationMixin(BaseMixin):
def __init__(self):
print("ValidationMixin.__init__")
super().__init__()
class User(LoggingMixin, ValidationMixin):
def __init__(self, name):
print("User.__init__")
super().__init__()
self.name = name
# What order will these print in?
user = User("Alice")
"I expected BaseMixin to be called twice - once through LoggingMixin and once through ValidationMixin. But look at the output:"
User.__init__
LoggingMixin.__init__
ValidationMixin.__init__
BaseMixin.__init__
Timothy quickly added a check to see what was happening:
print("\nUser MRO:")
for i, cls in enumerate(User.__mro__, 1):
print(f" {i}. {cls.__name__}")
Output:
User MRO:
1. User
2. LoggingMixin
3. ValidationMixin
4. BaseMixin
5. object
"See? BaseMixin only ran once! And ValidationMixin ran even though User doesn't directly call it. The MRO shows ValidationMixin comes after LoggingMixin. How does super() know to call ValidationMixin when LoggingMixin's super() is called? This makes no sense!"
Margaret leaned back with a knowing smile. "Welcome to the MRO - the Method Resolution Order. It's Python's solution to one of object-oriented programming's oldest problems: the diamond inheritance problem. What you're seeing isn't a bug - it's one of Python's most elegant design features."
"Elegant?" Timothy looked skeptical. "It looks like chaos!"
"It looks like chaos because you're thinking of super() as 'call my parent class,'" Margaret said. "But here's the secret: super() doesn't call your parent class. It calls the next class in the MRO chain. And that chain is carefully constructed using an algorithm called C3 linearization to ensure every class is called exactly once, in a sensible order."
She leaned forward. "This isn't just about inheritance. It's about how Python resolves every method call, how mixins work, why Django and Flask can have such clean plugin systems, and why multiple inheritance actually works in Python when it's a nightmare in other languages. Let me show you the mystery of the MRO."
She paused, then added: "We'll start with why the diamond problem exists, then we'll uncover how Python solves it with the MRO. You'll learn how to read the MRO chain, how super() really works, how to design cooperative inheritance correctly, and when to avoid multiple inheritance altogether. By the end, you'll understand why your code behaves the way it does - and how to use multiple inheritance like a pro."
The Diamond Problem: Why Inheritance Gets Messy
Margaret pulled up a classic example:
"""
THE DIAMOND PROBLEM:
The classic multiple inheritance issue that every OOP language must solve.
THE SCENARIO:
Shape
/ \
/ \
Square Circle
\ /
\ /
RoundedSquare
QUESTION: If RoundedSquare calls a method, which path does it follow?
- Through Square to Shape?
- Through Circle to Shape?
- Both paths (calling Shape twice)?
- Some other order?
DIFFERENT LANGUAGES, DIFFERENT SOLUTIONS:
- C++: Calls Shape twice (unless virtual inheritance)
- Java: Doesn't allow multiple inheritance of classes
- JavaScript: Prototype chain (single inheritance only)
- Python: Uses MRO (Method Resolution Order) with C3 linearization
PYTHON'S SOLUTION:
Every class has a linear order of ancestors (the MRO).
Method calls follow this order, ensuring each class appears exactly once.
"""
class Shape:
def __init__(self):
print(f" Shape.__init__ called from {self.__class__.__name__}")
super().__init__() # Surprise: calls next in MRO, not object!
def area(self):
return 0
class Square(Shape):
def __init__(self, side):
print(f" Square.__init__ called")
self.side = side
super().__init__()
def area(self):
return self.side ** 2
class Circle(Shape):
def __init__(self, radius):
print(f" Circle.__init__ called")
self.radius = radius
super().__init__()
def area(self):
import math
return math.pi * self.radius ** 2
class RoundedSquare(Square, Circle):
def __init__(self, side, radius):
print(f" RoundedSquare.__init__ called")
super().__init__(side) # What happens here?
print("Creating RoundedSquare:")
print("What order will the __init__ methods be called?\n")
rs = RoundedSquare(10, 5)
Output:
Creating RoundedSquare:
What order will the __init__ methods be called?
RoundedSquare.__init__ called
Square.__init__ called
Circle.__init__ called
Shape.__init__ called from RoundedSquare
Timothy studied the output. "Wait - RoundedSquare calls super().__init__() which calls Square. But then Square's super().__init__() calls Circle, not Shape! How does it know to go to Circle next?"
"That's the MRO at work," Margaret said. "Let me show you how to see it."
Viewing the MRO: The __mro__ Attribute
Margaret demonstrated how to inspect the method resolution order:
def show_mro(cls):
"""Display the Method Resolution Order for a class"""
print(f"\nMRO for {cls.__name__}:")
print("=" * 50)
# Using __mro__ attribute (tuple)
print("Via __mro__ attribute:")
for i, c in enumerate(cls.__mro__, 1):
print(f" {i}. {c.__name__}")
# Using mro() method (returns list)
print("\nVia mro() method:")
for i, c in enumerate(cls.mro(), 1):
print(f" {i}. {c.__name__}")
# Textual representation
print(f"\nTextual: {' -> '.join(c.__name__ for c in cls.__mro__)}")
# Show MRO for our diamond hierarchy
show_mro(RoundedSquare)
show_mro(Square)
show_mro(Circle)
show_mro(Shape)
Output:
MRO for RoundedSquare:
==================================================
Via __mro__ attribute:
1. RoundedSquare
2. Square
3. Circle
4. Shape
5. object
Via mro() method:
1. RoundedSquare
2. Square
3. Circle
4. Shape
5. object
Textual: RoundedSquare -> Square -> Circle -> Shape -> object
MRO for Square:
==================================================
Via __mro__ attribute:
1. Square
2. Shape
3. object
Via mro() method:
1. Square
2. Shape
3. object
Textual: Square -> Shape -> object
MRO for Circle:
==================================================
Via __mro__ attribute:
1. Circle
2. Shape
3. object
Via mro() method:
1. Circle
2. Shape
3. object
Textual: Circle -> Shape -> object
MRO for Shape:
==================================================
Via __mro__ attribute:
1. Shape
2. object
Via mro() method:
1. Shape
2. object
Textual: Shape -> object
"Aha!" Timothy exclaimed. "The MRO for RoundedSquare is: RoundedSquare → Square → Circle → Shape → object. So when Square calls super().__init__(), it's not calling its parent Shape - it's calling the next class in RoundedSquare's MRO, which is Circle!"
"Exactly!" Margaret confirmed. "The MRO is determined when the class is defined. Every time you call super(), Python looks up what class you're in, finds it in the MRO chain, and calls the next one. This ensures every class is called exactly once, in a predictable order."
"But how does Python decide this order?" Timothy asked. "Why Square before Circle?"
"That's where it gets interesting. Python uses an algorithm called C3 linearization."
C3 Linearization: The Algorithm Behind MRO
Margaret opened a detailed explanation:
"""
C3 LINEARIZATION ALGORITHM:
Python uses C3 linearization (from Dylan language, 1996) to compute MRO.
It ensures three critical properties:
1. CHILDREN BEFORE PARENTS
A class always appears before its parent classes in the MRO
2. PARENT ORDER PRESERVED
If class C inherits from (A, B), then A appears before B in C's MRO
3. MONOTONICITY
If class C is in class B's MRO, and B is in A's MRO,
then C must be in A's MRO in the same relative position
THE ALGORITHM (SIMPLIFIED):
For a class C with parents (Base1, Base2, ...):
1. Start with C itself
2. Merge the MROs of all parents
3. Add the parent classes themselves
4. Follow these rules during merge:
- Pick the first head that doesn't appear later in any other list
- If no such head exists, the inheritance is invalid
- Remove picked head from all lists
EXAMPLE: RoundedSquare(Square, Circle)
Step 1: Start with [RoundedSquare]
Step 2: Get parent MROs:
Square MRO: [Square, Shape, object]
Circle MRO: [Circle, Shape, object]
Parents list: [Square, Circle]
Step 3: Merge:
Lists: [Square, Shape, object], [Circle, Shape, object], [Square, Circle]
Pick Square (first head, doesn't appear in other lists' tails)
→ [RoundedSquare, Square]
Lists: [Shape, object], [Circle, Shape, object], [Circle]
Pick Circle (Shape appears later in second list, so skip to Circle)
→ [RoundedSquare, Square, Circle]
Lists: [Shape, object], [Shape, object]
Pick Shape (first head that works)
→ [RoundedSquare, Square, Circle, Shape]
Lists: [object], [object]
Pick object
→ [RoundedSquare, Square, Circle, Shape, object]
RESULT: RoundedSquare → Square → Circle → Shape → object
"""
class C3Visualizer:
"""Visualize C3 linearization algorithm"""
@staticmethod
def show_merge_process(cls):
"""Show step-by-step C3 merge for a class"""
print(f"\nC3 Linearization for {cls.__name__}:")
print("=" * 60)
# Show the input
print(f"Class: {cls.__name__}")
bases = cls.__bases__
print(f"Parents: {', '.join(b.__name__ for b in bases)}")
print("\nParent MROs:")
for base in bases:
mro_str = ' -> '.join(c.__name__ for c in base.__mro__)
print(f" {base.__name__}: {mro_str}")
print(f"\nFinal MRO:")
mro_str = ' -> '.join(c.__name__ for c in cls.__mro__)
print(f" {mro_str}")
print("\nKey observations:")
print(f" ✓ {cls.__name__} appears first (the class itself)")
for i, base in enumerate(bases, 1):
print(f" ✓ {base.__name__} appears before later parents")
print(f" ✓ Each class appears exactly once")
print(f" ✓ Parent order from class definition is preserved when possible")
# Demonstrate
C3Visualizer.show_merge_process(RoundedSquare)
Output:
C3 Linearization for RoundedSquare:
============================================================
Class: RoundedSquare
Parents: Square, Circle
Parent MROs:
Square: Square -> Shape -> object
Circle: Circle -> Shape -> object
Final MRO:
RoundedSquare -> Square -> Circle -> Shape -> object
Key observations:
✓ RoundedSquare appears first (the class itself)
✓ Square appears before later parents
✓ Circle appears before later parents
✓ Each class appears exactly once
✓ Parent order from class definition is preserved when possible
Timothy was following along. "So the algorithm ensures Square comes before Circle because that's the order in class RoundedSquare(Square, Circle). And both come before Shape because children always come before parents. That makes sense!"
"Right," Margaret said. "But here's where it gets tricky. What if the inheritance hierarchy is inconsistent?"
When MRO Fails: Inconsistent Hierarchies
Margaret showed an example of invalid inheritance:
"""
WHEN C3 LINEARIZATION FAILS:
The algorithm can fail if the inheritance hierarchy is inconsistent.
This prevents nonsensical MROs.
"""
class X:
pass
class Y:
pass
class A(X, Y): # A says: X before Y
pass
class B(Y, X): # B says: Y before X (contradiction!)
pass
# This will fail!
try:
class C(A, B): # Can't satisfy both A and B's ordering
pass
except TypeError as e:
print("MRO Error!")
print(f" {e}")
print("\nWhy this fails:")
print(" A's MRO: A -> X -> Y -> object")
print(" B's MRO: B -> Y -> X -> object")
print(" A says X before Y, but B says Y before X")
print(" No valid linearization exists!")
Output:
MRO Error!
Cannot create a consistent method resolution
order (MRO) for bases X, Y
Why this fails:
A's MRO: A -> X -> Y -> object
B's MRO: B -> Y -> X -> object
A says X before Y, but B says Y before X
No valid linearization exists!
"So Python prevents you from creating impossible inheritance hierarchies," Timothy observed. "That's actually really helpful - it catches design mistakes at class definition time."
"Exactly. Now let's talk about the real revelation: how super() actually works."
How Super() Really Works
Margaret pulled up a comprehensive explanation:
"""
THE SUPER() REVELATION:
super() doesn't mean "call my parent class"
super() means "call the next class in the MRO"
This is CRITICAL for cooperative multiple inheritance.
KEY INSIGHTS:
1. super() is bound to the MRO of the INSTANCE, not the class
2. super() finds the current class in the MRO and calls the next one
3. This allows mixins to cooperate without knowing about each other
4. Every class should call super() for cooperative inheritance
"""
class Demonstrator:
"""Show exactly what super() is doing"""
def __init__(self):
cls_name = self.__class__.__name__
current_class = type(self).__mro__[0]
print(f"\n{self.__class__.__name__}.__init__ executing")
print(f" Instance type: {type(self).__name__}")
print(f" Instance MRO: {' -> '.join(c.__name__ for c in type(self).__mro__)}")
# What would super() do here?
for i, cls in enumerate(type(self).__mro__):
if cls.__name__ == self.__class__.__name__:
if i + 1 < len(type(self).__mro__):
next_cls = type(self).__mro__[i + 1]
print(f" super() from {cls.__name__} will call: {next_cls.__name__}")
break
class A(Demonstrator):
def __init__(self):
print("\nA.__init__")
super().__init__()
class B(Demonstrator):
def __init__(self):
print("\nB.__init__")
super().__init__()
class C(A, B):
def __init__(self):
print("\nC.__init__")
super().__init__()
print("Creating instance of C:")
print("=" * 60)
c = C()
print("\n" + "=" * 60)
print("MRO Breakdown:")
for i, cls in enumerate(C.__mro__):
print(f" {i}. {cls.__name__}")
Output:
Creating instance of C:
============================================================
C.__init__
C.__init__ executing
Instance type: C
Instance MRO: C -> A -> B -> Demonstrator -> object
super() from C will call: A
A.__init__
A.__init__ executing
Instance type: C
Instance MRO: C -> A -> B -> Demonstrator -> object
super() from A will call: B
B.__init__
B.__init__ executing
Instance type: C
Instance MRO: C -> A -> B -> Demonstrator -> object
super() from B will call: Demonstrator
Demonstrator.__init__ executing
Instance type: C
Instance MRO: C -> A -> B -> Demonstrator -> object
super() from Demonstrator will call: object
============================================================
MRO Breakdown:
0. C
1. A
2. B
3. Demonstrator
4. object
"Oh wow!" Timothy exclaimed. "Even though A doesn't inherit from B, when an instance of C calls super() from within A's init, it calls B! Because the instance is of type C, and C's MRO has B after A."
"Exactly!" Margaret confirmed. "This is the key to cooperative multiple inheritance. Let me show you a practical example with mixins."
Practical Mixin Patterns: Cooperative Inheritance
Margaret demonstrated real-world mixin usage:
"""
MIXIN DESIGN PATTERN:
Mixins are classes designed to be combined via multiple inheritance.
They add specific functionality without being standalone classes.
RULES FOR GOOD MIXINS:
1. Always call super().__init__() even if you don't have a parent
2. Accept **kwargs and pass them to super()
3. Don't assume what other classes are in the MRO
4. Keep mixins focused (single responsibility)
5. Name them clearly (e.g., LoggingMixin, not Logger)
EXAMPLE: Django-style class-based view mixins
"""
class JSONResponseMixin:
"""Mixin to add JSON response capability"""
def __init__(self, **kwargs):
print(f" {self.__class__.__name__}: JSONResponseMixin.__init__")
super().__init__(**kwargs) # CRITICAL: pass kwargs along!
self.content_type = 'application/json'
def render_to_json(self, data):
import json
return json.dumps(data)
class CachingMixin:
"""Mixin to add caching capability"""
def __init__(self, cache_timeout=300, **kwargs):
print(f" {self.__class__.__name__}: CachingMixin.__init__")
super().__init__(**kwargs) # Pass remaining kwargs
self.cache_timeout = cache_timeout
def get_cache_key(self):
return f"cache_{self.__class__.__name__}"
class LoggingMixin:
"""Mixin to add logging capability"""
def __init__(self, log_level='INFO', **kwargs):
print(f" {self.__class__.__name__}: LoggingMixin.__init__")
super().__init__(**kwargs)
self.log_level = log_level
def log(self, message):
print(f"[{self.log_level}] {message}")
class BaseView:
"""Base view class"""
def __init__(self, template_name=None, **kwargs):
print(f" {self.__class__.__name__}: BaseView.__init__")
# Base class might not have super(), but safe to call
if kwargs:
super().__init__(**kwargs)
self.template_name = template_name
def render(self):
return "Rendering view"
class APIView(JSONResponseMixin, CachingMixin, LoggingMixin, BaseView):
"""API view combining multiple mixins"""
def __init__(self, **kwargs):
print(f" {self.__class__.__name__}: APIView.__init__")
super().__init__(**kwargs)
def get(self, request):
self.log("Processing GET request")
data = {"status": "success"}
return self.render_to_json(data)
print("Creating APIView with cooperative initialization:")
print("=" * 60)
view = APIView(
template_name='api.html',
cache_timeout=600,
log_level='DEBUG'
)
print("\n" + "=" * 60)
print("APIView MRO:")
for i, cls in enumerate(APIView.__mro__):
print(f" {i}. {cls.__name__}")
print("\n" + "=" * 60)
print("Testing the view:")
class FakeRequest:
pass
result = view.get(FakeRequest())
print(f"Result: {result}")
Output:
Creating APIView with cooperative initialization:
============================================================
APIView: APIView.__init__
APIView: JSONResponseMixin.__init__
APIView: CachingMixin.__init__
APIView: LoggingMixin.__init__
APIView: BaseView.__init__
============================================================
APIView MRO:
0. APIView
1. JSONResponseMixin
2. CachingMixin
3. LoggingMixin
4. BaseView
5. object
============================================================
Testing the view:
[DEBUG] Processing GET request
Result: {"status": "success"}
"Perfect!" Timothy said. "Each mixin calls super().__init__(**kwargs), passing along any kwargs it doesn't use. This allows all the mixins to cooperate without knowing about each other. The **kwargs pattern is the glue that makes it work."
"Exactly," Margaret confirmed. "Notice how each mixin accepts its own parameters plus **kwargs, processes what it needs, and passes the rest along. This is cooperative inheritance in action."
Common Mistakes: When MRO Goes Wrong
Margaret showed typical pitfalls:
"""
COMMON MRO MISTAKES:
1. Forgetting to call super()
2. Calling parent class directly instead of super()
3. Not passing **kwargs in mixins
4. Assuming MRO order without checking
"""
print("MISTAKE #1: Forgetting to call super()")
print("=" * 60)
class Mixin1:
def __init__(self):
print(" Mixin1.__init__")
super().__init__() # Good: calls next in MRO
class Mixin2:
def __init__(self):
print(" Mixin2.__init__")
# BAD: Forgot to call super()!
# This breaks the chain - object.__init__ never runs!
class Combined1(Mixin1, Mixin2):
def __init__(self):
print(" Combined1.__init__")
super().__init__()
c1 = Combined1()
print("\nResult: Chain broken! object.__init__ was skipped.")
print("Mixin2 ran, but didn't call super(), so nothing after it runs.")
print("\n" + "=" * 60)
print("MISTAKE #2: Calling parent directly instead of super()")
print("=" * 60)
class Parent:
def __init__(self):
print(" Parent.__init__")
class Mixin3:
def __init__(self):
print(" Mixin3.__init__")
super().__init__()
class Child(Mixin3, Parent):
def __init__(self):
print(" Child.__init__")
# BAD: Direct call breaks MRO chain
Parent.__init__(self) # Wrong!
# Mixin3 never gets called!
c2 = Child()
print("\nResult: Mixin3 skipped! Should use super() instead.")
print("\n" + "=" * 60)
print("MISTAKE #3: Not passing **kwargs")
print("=" * 60)
class ConfigMixin:
def __init__(self, config_file):
print(f" ConfigMixin.__init__(config_file={config_file})")
super().__init__() # BAD: Should pass **kwargs
class DataMixin:
def __init__(self, data_source):
print(f" DataMixin.__init__(data_source={data_source})")
super().__init__() # BAD: Should pass **kwargs
class Combined2(ConfigMixin, DataMixin):
def __init__(self):
print(" Combined2.__init__")
super().__init__(
config_file='app.cfg',
data_source='db.sqlite'
)
try:
c3 = Combined2()
except TypeError as e:
print(f"\nError: {e}")
print("Problem: Mixins don't accept/pass **kwargs!")
print("\n" + "=" * 60)
print("CORRECT VERSION:")
print("=" * 60)
class ConfigMixinFixed:
def __init__(self, config_file=None, **kwargs):
print(f" ConfigMixinFixed.__init__(config_file={config_file})")
super().__init__(**kwargs) # Pass remaining kwargs
class DataMixinFixed:
def __init__(self, data_source=None, **kwargs):
print(f" DataMixinFixed.__init__(data_source={data_source})")
super().__init__(**kwargs) # Pass remaining kwargs
class Combined3(ConfigMixinFixed, DataMixinFixed):
def __init__(self):
print(" Combined3.__init__")
super().__init__(
config_file='app.cfg',
data_source='db.sqlite'
)
c4 = Combined3()
print("\n✓ Works! All mixins receive their parameters.")
Output:
MISTAKE #1: Forgetting to call super()
============================================================
Combined1.__init__
Mixin1.__init__
Mixin2.__init__
Result: Chain broken! object.__init__ was skipped.
Mixin2 ran, but didn't call super(), so nothing after it runs.
============================================================
MISTAKE #2: Calling parent directly instead of super()
============================================================
Child.__init__
Parent.__init__
Result: Mixin3 skipped! Should use super() instead.
============================================================
MISTAKE #3: Not passing **kwargs
============================================================
Combined2.__init__
Error: ConfigMixin.__init__() got an unexpected keyword argument 'data_source'
Problem: Mixins don't accept/pass **kwargs!
============================================================
CORRECT VERSION:
============================================================
Combined3.__init__
ConfigMixinFixed.__init__(config_file=app.cfg)
DataMixinFixed.__init__(data_source=db.sqlite)
✓ Works! All mixins receive their parameters.
"These are all mistakes I've made!" Timothy admitted. "Especially calling the parent class directly instead of using super(). That breaks the whole MRO chain."
"Very common mistake," Margaret agreed. "The rule is simple: in cooperative multiple inheritance, always use super(), never call parent classes directly."
Method Resolution: Beyond init
Margaret showed that MRO affects all methods, not just __init__:
"""
MRO AFFECTS ALL METHOD CALLS:
The MRO isn't just for __init__ - it's used for EVERY attribute lookup.
This is how method overriding works.
"""
class Animal:
def speak(self):
return "Some sound"
def move(self):
return "Moving somehow"
class Mammal(Animal):
def speak(self):
return "Mammal sound"
def move(self):
result = super().move() # Call Animal's move
return f"Walking ({result})"
class Flyer:
def move(self):
return "Flying through air"
def fly(self):
return "Flapping wings"
class Bat(Mammal, Flyer):
def speak(self):
result = super().speak() # Which speak() is called?
return f"Screech! ({result})"
# Note: Bat doesn't override move()
# Which move() will be called?
print("Bat MRO:")
print(' -> '.join(cls.__name__ for cls in Bat.__mro__))
bat = Bat()
print(f"\nbat.speak(): {bat.speak()}")
print(f"bat.move(): {bat.move()}")
print(f"bat.fly(): {bat.fly()}")
print("\nMethod resolution:")
print(" bat.speak() -> Bat.speak() -> Mammal.speak() [via super()]")
print(" bat.move() -> Mammal.move() [first in MRO with move()]")
print(" bat.fly() -> Flyer.fly() [only class with fly()]")
print("\nKey insight: MRO determines which method is called")
print("for EVERY attribute access, not just __init__!")
Output:
Bat MRO:
Bat -> Mammal -> Flyer -> Animal -> object
bat.speak(): Screech! (Mammal sound)
bat.move(): Walking (Moving somehow)
bat.fly(): Flapping wings
Method resolution:
bat.speak() -> Bat.speak() -> Mammal.speak() [via super()]
bat.move() -> Mammal.move() [first in MRO with move()]
bat.fly() -> Flyer.fly() [only class with fly()]
Key insight: MRO determines which method is called
for EVERY attribute access, not just __init__!
"So even though Flyer has a move() method, Bat uses Mammal's move() because Mammal comes first in the MRO," Timothy observed.
"Exactly. The MRO determines the order for all attribute lookups. Python walks the MRO from left to right until it finds the attribute."
Debugging MRO Issues: Practical Troubleshooting
Margaret showed how to diagnose MRO problems:
"""
DEBUGGING MRO PROBLEMS:
Tools and techniques for understanding and fixing MRO issues.
"""
def debug_mro(cls):
"""Comprehensive MRO debugging information"""
print(f"\nDEBUGGING: {cls.__name__}")
print("=" * 70)
# Show the MRO
print("Method Resolution Order:")
for i, c in enumerate(cls.__mro__, 1):
print(f" {i}. {c.__name__} from {c.__module__}")
# Show direct bases
print(f"\nDirect parent classes (in declaration order):")
for i, base in enumerate(cls.__bases__, 1):
print(f" {i}. {base.__name__}")
# Show which methods come from where
print(f"\nMethod sources for key methods:")
for method_name in ['__init__', '__str__', '__repr__']:
if hasattr(cls, method_name):
method = getattr(cls, method_name)
# Find which class in MRO provides this method
for c in cls.__mro__:
if method_name in c.__dict__:
print(f" {method_name}: defined in {c.__name__}")
break
# Check for potential issues
print(f"\nPotential issues:")
# Check if any __init__ doesn't call super()
has_init_warning = False
for c in cls.__mro__[:-1]: # Skip object
if '__init__' in c.__dict__:
import inspect
source = inspect.getsource(c.__init__)
if 'super()' not in source and 'super(' not in source:
print(f" ⚠ {c.__name__}.__init__ doesn't call super()")
has_init_warning = True
if not has_init_warning:
print(f" ✓ All __init__ methods appear to call super()")
return cls.__mro__
# Example debugging session
class A:
def __init__(self):
print("A")
super().__init__()
class B:
def __init__(self):
print("B")
# Oops, forgot super()!
class C(A, B):
def __init__(self):
print("C")
super().__init__()
debug_mro(C)
print("\n" + "=" * 70)
print("Testing initialization:")
C()
Output:
DEBUGGING: C
======================================================================
Method Resolution Order:
1. C from __main__
2. A from __main__
3. B from __main__
4. object from builtins
Direct parent classes (in declaration order):
1. A
2. B
Method sources for key methods:
__init__: defined in C
__str__: defined in object
__repr__: defined in object
Potential issues:
⚠ B.__init__ doesn't call super()
======================================================================
Testing initialization:
C
A
B
"This debugging function is really helpful!" Timothy said. "It shows me exactly where each method comes from and warns about missing super() calls."
"It's essential for understanding complex inheritance hierarchies," Margaret agreed. "When things go wrong, always check the MRO first."
When to Avoid Multiple Inheritance: Modern Alternatives
Margaret shifted to a broader perspective:
"""
WHEN NOT TO USE MULTIPLE INHERITANCE:
Multiple inheritance is powerful but can be complex.
Modern Python offers alternatives that are often better.
ALTERNATIVES TO CONSIDER:
1. Composition (has-a instead of is-a)
2. Protocols (structural subtyping, PEP 544)
3. Dependency injection
4. Decorator pattern
DECISION GUIDE:
USE Multiple Inheritance When:
✓ Mixins add truly orthogonal functionality
✓ You need to combine behaviors in various ways
✓ Framework design (Django views, testing frameworks)
✓ You understand and control all base classes
AVOID Multiple Inheritance When:
✗ Classes have overlapping responsibilities
✗ You're inheriting from third-party classes you don't control
✗ Composition would be clearer
✗ You're fighting the MRO (reordering bases to make it work)
"""
print("ALTERNATIVE 1: Composition")
print("=" * 60)
# Instead of multiple inheritance:
class LoggerMixin:
def log(self, msg):
print(f"[LOG] {msg}")
class CacheMixin:
def cache_get(self, key):
return f"cached_{key}"
class ServiceWithMixins(LoggerMixin, CacheMixin):
def process(self):
self.log("Processing")
return self.cache_get("data")
# Use composition:
class Logger:
def log(self, msg):
print(f"[LOG] {msg}")
class Cache:
def get(self, key):
return f"cached_{key}"
class ServiceWithComposition:
def __init__(self):
self.logger = Logger()
self.cache = Cache()
def process(self):
self.logger.log("Processing")
return self.cache.get("data")
print("With mixins:")
s1 = ServiceWithMixins()
s1.process()
print("\nWith composition:")
s2 = ServiceWithComposition()
s2.process()
print("\n✓ Composition is more explicit and flexible")
print("\n" + "=" * 60)
print("ALTERNATIVE 2: Protocols (Python 3.8+)")
print("=" * 60)
from typing import Protocol
class Drawable(Protocol):
"""Protocol: any class with a draw() method"""
def draw(self) -> str:
...
class Circle:
def draw(self) -> str:
return "Drawing circle"
class Square:
def draw(self) -> str:
return "Drawing square"
def render(shape: Drawable) -> None:
"""Works with anything that has draw() method"""
print(shape.draw())
# No inheritance needed!
render(Circle())
render(Square())
print("\n✓ Protocols provide duck typing with type checking")
print("✓ No inheritance hierarchy needed")
print("\n" + "=" * 60)
print("WHEN MULTIPLE INHERITANCE SHINES:")
print("=" * 60)
# Django-style class-based views
class TemplateResponseMixin:
template_name = None
def render_to_response(self, context):
return f"Rendering {self.template_name} with {context}"
class ContextMixin:
def get_context_data(self, **kwargs):
return {'view': self.__class__.__name__, **kwargs}
class View:
def dispatch(self, request):
return self.get(request)
class TemplateView(TemplateResponseMixin, ContextMixin, View):
"""Combines mixins for a template-rendering view"""
template_name = 'home.html'
def get(self, request):
context = self.get_context_data(user='Alice')
return self.render_to_response(context)
view = TemplateView()
result = view.dispatch("fake_request")
print(result)
print("\n✓ Multiple inheritance perfect for framework extension points")
print("✓ Mixins provide reusable, composable functionality")
Output:
ALTERNATIVE 1: Composition
============================================================
With mixins:
[LOG] Processing
With composition:
[LOG] Processing
✓ Composition is more explicit and flexible
============================================================
ALTERNATIVE 2: Protocols (Python 3.8+)
============================================================
Drawing circle
Drawing square
✓ Protocols provide duck typing with type checking
✓ No inheritance hierarchy needed
============================================================
WHEN MULTIPLE INHERITANCE SHINES:
============================================================
Rendering home.html with {'view': 'TemplateView', 'user': 'Alice'}
✓ Multiple inheritance perfect for framework extension points
✓ Mixins provide reusable, composable functionality
"So the modern approach is: prefer composition, use protocols for interfaces, and save multiple inheritance for when you really need it," Timothy summarized.
"Exactly. Multiple inheritance is a tool, not a default. Use it when it's the right tool."
Real-World Examples: MRO in Action
Margaret showed how popular frameworks use MRO:
"""
REAL-WORLD MRO USAGE:
How major Python frameworks leverage the MRO.
"""
print("EXAMPLE 1: Django Class-Based Views")
print("=" * 60)
# Simplified Django-style view hierarchy
class View:
"""Base view class"""
def dispatch(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower())
return handler(request, *args, **kwargs)
class TemplateResponseMixin:
"""Mixin for rendering templates"""
template_name = None
def render_to_response(self, context):
return f"<html>Rendered {self.template_name}</html>"
class ContextMixin:
"""Mixin for building context"""
def get_context_data(self, **kwargs):
return kwargs
class SingleObjectMixin:
"""Mixin for fetching single object"""
model = None
def get_object(self):
return f"{self.model} instance"
class DetailView(SingleObjectMixin, TemplateResponseMixin, ContextMixin, View):
"""Combines all mixins for detail view"""
def get(self, request, *args, **kwargs):
obj = self.get_object()
context = self.get_context_data(object=obj)
return self.render_to_response(context)
class UserDetailView(DetailView):
"""Concrete view for user details"""
model = "User"
template_name = "user_detail.html"
print("UserDetailView MRO:")
for i, cls in enumerate(UserDetailView.__mro__, 1):
print(f" {i}. {cls.__name__}")
print("\n" + "=" * 60)
print("EXAMPLE 2: unittest.TestCase Mixins")
print("=" * 60)
# Simplified unittest-style test mixins
class TestCase:
"""Base test case"""
def setUp(self):
pass
def tearDown(self):
pass
def run_test(self):
self.setUp()
# run test
self.tearDown()
class DatabaseMixin:
"""Mixin for database tests"""
def setUp(self):
print(" Setting up database")
super().setUp()
def tearDown(self):
super().tearDown()
print(" Tearing down database")
class APIMixin:
"""Mixin for API tests"""
def setUp(self):
print(" Setting up API client")
super().setUp()
def tearDown(self):
super().tearDown()
print(" Cleaning up API")
class UserAPITest(DatabaseMixin, APIMixin, TestCase):
"""Test combining database and API setup"""
pass
print("UserAPITest MRO:")
for i, cls in enumerate(UserAPITest.__mro__, 1):
print(f" {i}. {cls.__name__}")
print("\nRunning setup/teardown:")
test = UserAPITest()
test.setUp()
test.tearDown()
print("\n✓ Setup runs in MRO order, teardown in reverse")
print("✓ Each mixin contributes to test environment")
Output:
EXAMPLE 1: Django Class-Based Views
============================================================
UserDetailView MRO:
1. UserDetailView
2. DetailView
3. SingleObjectMixin
4. TemplateResponseMixin
5. ContextMixin
6. View
7. object
============================================================
EXAMPLE 2: unittest.TestCase Mixins
============================================================
UserAPITest MRO:
1. UserAPITest
2. DatabaseMixin
3. APIMixin
4. TestCase
5. object
Running setup/teardown:
Setting up database
Setting up API client
Cleaning up API
Tearing down database
✓ Setup runs in MRO order, teardown in reverse
✓ Each mixin contributes to test environment
"I use Django views all the time and never realized how much they depend on the MRO!" Timothy exclaimed.
"Most Django developers don't," Margaret smiled. "But understanding the MRO helps you know which mixin to override and where to place custom mixins in the inheritance list."
The Decision Tree: When and How to Use MRO
Margaret provided comprehensive guidelines:
"""
MRO DECISION TREE AND BEST PRACTICES
START HERE: Do you need multiple inheritance?
│
├─NO → Use composition or protocols
│ ✓ More explicit
│ ✓ More flexible
│ ✓ Easier to understand
│
└─YES → Follow these rules:
│
├─1. DESIGN MIXINS PROPERLY
│ ✓ Each mixin has single responsibility
│ ✓ Always call super() in __init__
│ ✓ Accept and pass **kwargs
│ ✓ Don't assume other classes in MRO
│ ✓ Name clearly (SomethingMixin)
│
├─2. ORDER BASES CAREFULLY
│ ✓ Most specific first (left to right)
│ ✓ Mixins before base classes
│ ✓ Check resulting MRO makes sense
│ ✓ Test initialization order
│
├─3. DOCUMENT THE MRO
│ ✓ Show expected MRO in docstring
│ ✓ Explain why order matters
│ ✓ Note which methods each mixin provides
│
├─4. TEST THOROUGHLY
│ ✓ Test with different mixin combinations
│ ✓ Verify initialization order
│ ✓ Check method resolution
│ ✓ Test edge cases
│
└─5. KNOW WHEN TO STOP
✓ More than 3-4 bases gets complex
✓ If you're fighting MRO, rethink design
✓ Consider composition instead
MIXIN DESIGN TEMPLATE:
class WellDesignedMixin:
'''
Mixin that adds [specific functionality].
Expected MRO position: Before [BaseClass]
Provides methods: method1(), method2()
Requires from other classes: Nothing (cooperative)
'''
def __init__(self, mixin_param=default, **kwargs):
# Process this mixin's parameters
self.mixin_param = mixin_param
# ALWAYS call super() with remaining kwargs
super().__init__(**kwargs)
def method1(self):
# Mixin-specific functionality
pass
def method2(self):
# Can call super() if needed
result = super().method2() if hasattr(super(), 'method2') else None
# Add mixin behavior
return result
QUICK REFERENCE:
✓ DO:
- Use super() everywhere
- Pass **kwargs in __init__
- Keep mixins focused
- Document expected MRO
- Test combinations
✗ DON'T:
- Call parent classes directly
- Assume MRO order without checking
- Create deep hierarchies (>3-4 levels)
- Mix unrelated concerns in one mixin
- Use multiple inheritance as default
"""
def validate_mixin_design(cls):
"""Helper to check if a class follows mixin best practices"""
issues = []
# Check if it's named appropriately
if not cls.__name__.endswith('Mixin') and cls.__name__ != 'object':
issues.append(f"⚠ Consider naming {cls.__name__} as {cls.__name__}Mixin")
# Check if __init__ exists and calls super()
if '__init__' in cls.__dict__:
import inspect
try:
source = inspect.getsource(cls.__init__)
if 'super()' not in source:
issues.append(f"⚠ {cls.__name__}.__init__ should call super()")
if '**kwargs' not in source:
issues.append(f"⚠ {cls.__name__}.__init__ should accept **kwargs")
except:
pass
if not issues:
print(f"✓ {cls.__name__} follows best practices")
else:
print(f"Issues with {cls.__name__}:")
for issue in issues:
print(f" {issue}")
return len(issues) == 0
# Example validation
class GoodMixin:
def __init__(self, param=None, **kwargs):
self.param = param
super().__init__(**kwargs)
class BadMixin:
def __init__(self, param):
self.param = param
# Missing super() call
# Missing **kwargs
print("Validating mixin designs:")
validate_mixin_design(GoodMixin)
validate_mixin_design(BadMixin)
Output:
Validating mixin designs:
✓ GoodMixin follows best practices
Issues with BadMixin:
⚠ BadMixin.__init__ should call super()
⚠ BadMixin.__init__ should accept **kwargs
"This decision tree and checklist are fantastic!" Timothy said. "I wish I'd had this when I started using mixins."
"Most Python developers learn MRO the hard way," Margaret agreed. "Now you have a systematic approach."
Key Takeaways: Mastering the MRO
Margaret brought everything together:
"""
MRO MASTER SUMMARY
═══════════════════════════════════════════════════════════════════
1. WHAT IS THE MRO
═══════════════════════════════════════════════════════════════════
The Method Resolution Order is the linear sequence of classes
Python searches when looking up attributes (methods, properties).
- Every class has an MRO (stored in __mro__ attribute)
- Determines which method is called for inheritance
- Computed at class definition time using C3 linearization
- Ensures each class appears exactly once
- Guarantees sensible ordering (children before parents)
═══════════════════════════════════════════════════════════════════
2. THE DIAMOND PROBLEM
═══════════════════════════════════════════════════════════════════
Classic multiple inheritance issue:
Base
/ \
A B
\ /
C
Question: When C calls a method, which path to Base?
Python's answer: C → A → B → Base (single, predictable path)
✓ Each class called exactly once
✓ No ambiguity about method resolution
✓ Predictable, deterministic behavior
═══════════════════════════════════════════════════════════════════
3. C3 LINEARIZATION ALGORITHM
═══════════════════════════════════════════════════════════════════
How Python computes the MRO:
Rules:
1. Children before parents (depth-first)
2. Parent order preserved from class definition
3. Monotonicity (consistent positioning)
class C(A, B): # A before B in MRO
Properties:
✓ Guarantees sensible order
✓ Prevents inconsistent hierarchies
✓ Fails at class definition if impossible
✓ Same algorithm as Dylan, Perl 6
═══════════════════════════════════════════════════════════════════
4. HOW SUPER() REALLY WORKS
═══════════════════════════════════════════════════════════════════
Critical insight: super() ≠ "call my parent"
super() means: "call the next class in the MRO"
- Bound to the instance's MRO, not the class
- Finds current class in MRO, calls next one
- Enables cooperative multiple inheritance
- Why mixins can work without knowing each other
Example:
class A(B):
def method(self):
super().method() # Calls next in MRO, not B!
If instance is type C(A, X), super() in A calls X, not B!
═══════════════════════════════════════════════════════════════════
5. VIEWING THE MRO
═══════════════════════════════════════════════════════════════════
Tools to inspect MRO:
# View as tuple
MyClass.__mro__
# View as list
MyClass.mro()
# Pretty print
for cls in MyClass.__mro__:
print(cls.__name__)
# From instance
instance.__class__.__mro__
type(instance).__mro__
═══════════════════════════════════════════════════════════════════
6. COOPERATIVE MULTIPLE INHERITANCE
═══════════════════════════════════════════════════════════════════
Pattern for mixins that work together:
Rules:
✓ Always call super().__init__() in __init__
✓ Accept **kwargs and pass them along
✓ Don't assume what else is in the MRO
✓ Keep mixins focused (single responsibility)
✓ Test with different combinations
Template:
class MyMixin:
def __init__(self, my_param=default, **kwargs):
self.my_param = my_param
super().__init__(**kwargs) # Critical!
═══════════════════════════════════════════════════════════════════
7. COMMON MISTAKES
═══════════════════════════════════════════════════════════════════
✗ Forgetting to call super()
→ Breaks the MRO chain, skips classes
✗ Calling parent directly: Parent.__init__(self)
→ Bypasses MRO, breaks cooperative inheritance
✗ Not passing **kwargs
→ Later mixins don't get their parameters
✗ Assuming MRO order without checking
→ Surprises when adding new mixins
✗ Too many levels of inheritance
→ Complex, hard to understand
═══════════════════════════════════════════════════════════════════
8. WHEN TO USE MULTIPLE INHERITANCE
═══════════════════════════════════════════════════════════════════
USE when:
✓ Mixins add orthogonal functionality
✓ Framework extension points (Django views)
✓ Need various combinations of behaviors
✓ Following established patterns
AVOID when:
✗ Composition would be clearer
✗ Classes have overlapping concerns
✗ Inheriting from third-party classes
✗ Fighting the MRO (reordering to make it work)
Modern alternatives:
→ Composition (has-a relationships)
→ Protocols (structural subtyping)
→ Dependency injection
═══════════════════════════════════════════════════════════════════
9. MIXIN DESIGN BEST PRACTICES
═══════════════════════════════════════════════════════════════════
Good mixin characteristics:
✓ Single, focused responsibility
✓ Named clearly (SomethingMixin)
✓ Documented (what it provides, where in MRO)
✓ Cooperative (__init__ calls super with **kwargs)
✓ No assumptions about other classes
✓ Works in various combinations
Ordering:
- Most specific first (left to right)
- Mixins before base classes
- Check resulting MRO makes sense
═══════════════════════════════════════════════════════════════════
10. REAL-WORLD USAGE
═══════════════════════════════════════════════════════════════════
Frameworks that rely on MRO:
Django:
- Class-based views (DetailView, ListView, etc.)
- Form mixins (FormMixin, ModelFormMixin)
- Authentication mixins (LoginRequiredMixin)
unittest:
- TestCase mixins for setup/teardown
- Database test mixins
- API test helpers
Flask:
- View mixins for common functionality
- Extension mixins
Key insight: MRO enables plugin architectures
═══════════════════════════════════════════════════════════════════
11. DEBUGGING MRO ISSUES
═══════════════════════════════════════════════════════════════════
When things go wrong:
1. Print the MRO
print(MyClass.__mro__)
2. Check for missing super() calls
Grep for __init__ without super()
3. Verify parent order
Check class Definition: class C(A, B)
Verify A appears before B in MRO
4. Test initialization
Add print statements in __init__
Verify all classes initialized
5. Check for **kwargs
Ensure parameters flow through chain
═══════════════════════════════════════════════════════════════════
12. QUICK REFERENCE
═══════════════════════════════════════════════════════════════════
# View MRO
cls.__mro__ # Tuple of classes
cls.mro() # List of classes
# In __init__
super().__init__(**kwargs) # Always do this in mixins
# Check if method exists
if hasattr(super(), 'method'):
super().method()
# Order bases
class Child(Mixin1, Mixin2, Base): # Mixins first
# Validate design
- Each __init__ calls super()
- All __init__ accept **kwargs
- Mixins don't assume other classes
- MRO makes logical sense
═══════════════════════════════════════════════════════════════════
13. THE BIG PICTURE
═══════════════════════════════════════════════════════════════════
The MRO is Python's elegant solution to multiple inheritance:
- Predictable method resolution
- No ambiguity or conflicts
- Enables cooperative inheritance
- Powers framework extension systems
- Makes mixins possible
Understanding MRO means understanding:
- Why super() works the way it does
- How to design mixins properly
- When to use multiple inheritance
- How frameworks like Django work
- When to use alternatives
═══════════════════════════════════════════════════════════════════
14. BOTTOM LINE
═══════════════════════════════════════════════════════════════════
The MRO isn't just about inheritance - it's about composability.
✓ Learn to read MRO chains
✓ Always use super() in cooperative inheritance
✓ Design mixins carefully
✓ Document expected MRO
✓ Test combinations thoroughly
✓ Know when composition is better
With MRO mastery, you can:
- Use Django/Flask view mixins confidently
- Design reusable mixins
- Debug inheritance issues quickly
- Know when multiple inheritance is appropriate
- Understand how super() really works
The diamond problem? Solved by Python's MRO.
"""
Timothy leaned back, finally grasping the complete picture. "So the MRO isn't just a technical detail - it's what makes multiple inheritance actually work in Python. The C3 linearization algorithm ensures a sensible, predictable order. super() follows this order, not parent classes directly. And this enables cooperative inheritance where mixins can work together without knowing about each other."
"Perfect understanding," Margaret confirmed. "The MRO is one of Python's most elegant design features. It solved the diamond problem that plagued other languages, enabled powerful patterns like Django's class-based views, and made mixins a viable design pattern."
She continued, "Most developers use Python for years without understanding the MRO. They cargo-cult super() calls and get confused when inheritance doesn't work as expected. But now you understand the mechanism. You know that super() isn't 'call my parent' - it's 'call the next class in the instance's MRO.' You know that the MRO is computed using C3 linearization to ensure a consistent, sensible order. You know how to design cooperative mixins that work in any combination."
"And I know when not to use multiple inheritance," Timothy added. "Composition is often clearer. Protocols can replace inheritance for interfaces. Multiple inheritance is a tool for specific situations - like framework extension points - not a default."
"Exactly," Margaret smiled. "The secret of the MRO is this: Python's multiple inheritance isn't broken or confusing - it's carefully designed with specific rules. Understanding those rules turns a 'mysterious' feature into a powerful tool."
"The diamond problem that stumped other languages? Python solved it with the MRO. The ambiguity about which method to call? Resolved by C3 linearization. The challenge of cooperative mixins? Enabled by super() following the MRO chain. Every apparent complexity has a reason, and understanding the MRO reveals the elegant simplicity underneath."
With that knowledge, Timothy could now:
- Read and understand any class's MRO
- Design cooperative mixins that work in combinations
- Use
super()correctly in multiple inheritance - Understand Django's class-based views architecture
- Debug inheritance issues systematically
- Know when to use multiple inheritance vs alternatives
- Explain the MRO to other developers clearly
The MRO wasn't a mystery anymore - it was a well-understood design feature that made Python's object system powerful and predictable.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (0)