Cooperative Multiple Inheritance — Deep Dive
Why Cooperative Inheritance Exists
Languages like Java avoid multiple inheritance entirely (using interfaces instead). C++ allows it but doesn’t enforce cooperation, leading to the “deadly diamond of death.” Python took a middle path: allow multiple inheritance, but provide super() as a protocol for cooperative method dispatch.
The result is a powerful system that enables mixin patterns, framework extensibility, and aspect-oriented-style programming — but it requires every participant to follow the rules.
C3 Linearization: How the MRO Is Computed
Python’s MRO uses the C3 linearization algorithm (introduced in Python 2.3). It guarantees two properties:
- Local precedence order: If a class lists parents as
(A, B), thenAcomes beforeBin the MRO. - Monotonicity: If
Acomes beforeBin a parent’s MRO,Acomes beforeBin the child’s MRO too.
The Algorithm
For a class C(B1, B2, ..., Bn):
L[C] = C + merge(L[B1], L[B2], ..., L[Bn], [B1, B2, ..., Bn])
The merge operation repeatedly:
- Takes the first element of the first list.
- Checks that this element doesn’t appear in the tail of any other list.
- If clean, adds it to the result and removes it from all lists.
- If not clean, moves to the next list.
If no list offers a clean head, C3 fails and Python raises TypeError: Cannot create a consistent method resolution order.
Practical Example
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(D.__mro__)
# (D, B, C, A, object)
The linearization ensures B comes before C (because D lists B first), and both come before A (because both inherit from A).
The super() Protocol in Detail
super() is not syntactic sugar for “call parent.” It’s a descriptor that:
- Inspects the MRO of the instance’s class.
- Finds the current class in the MRO.
- Returns a proxy that delegates to the next class in the MRO.
class A:
def method(self):
print("A.method")
super().method() # Calls next in MRO, which might be B, not object
class B:
def method(self):
print("B.method")
super().method() # Calls object.method (if it exists)
class C(A, B):
def method(self):
print("C.method")
super().method() # Calls A.method
c = C()
c.method()
# Output:
# C.method
# A.method
# B.method
Notice: A.method() calls B.method() via super(), even though A doesn’t inherit from B. This is cooperative dispatch — the MRO of C determines the chain.
Production Pattern: Robust Mixin Design
Well-designed mixins follow strict rules for cooperation:
class LoggingMixin:
"""Mixin that logs method calls. Cooperates via super()."""
def save(self, **kwargs):
print(f"LOG: Saving {self.__class__.__name__}")
result = super().save(**kwargs)
print(f"LOG: Saved {self.__class__.__name__}")
return result
class ValidationMixin:
"""Mixin that validates before saving. Cooperates via super()."""
def save(self, **kwargs):
self.validate()
return super().save(**kwargs)
def validate(self):
for field, rules in getattr(self, '_validators', {}).items():
value = getattr(self, field, None)
for rule in rules:
rule(value)
class BaseModel:
"""Concrete base. Terminal in the super() chain."""
def save(self, **kwargs):
print(f"SAVE: {self.__class__.__name__} to database")
return True
class User(LoggingMixin, ValidationMixin, BaseModel):
_validators = {
'email': [lambda v: None if '@' in str(v) else (_ for _ in ()).throw(ValueError("Invalid email"))]
}
def __init__(self, email):
self.email = email
Call order for User().save():
LoggingMixin.save— logs entryValidationMixin.save— validates fieldsBaseModel.save— persists to database- Back up the chain for logging exit
Constructor Cooperation: The **kwargs Protocol
The hardest part of cooperative inheritance is constructors, because different classes need different arguments:
class Identifiable:
def __init__(self, *, id: str, **kwargs):
super().__init__(**kwargs)
self.id = id
class Timestamped:
def __init__(self, *, created_at: str = "", **kwargs):
super().__init__(**kwargs)
self.created_at = created_at or "now"
class Named:
def __init__(self, *, name: str, **kwargs):
super().__init__(**kwargs)
self.name = name
class Entity(Identifiable, Timestamped, Named):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# All keyword arguments flow through the chain
e = Entity(id="abc", name="Widget", created_at="2026-01-01")
print(e.id, e.name, e.created_at) # abc Widget 2026-01-01
Rules for safe constructors:
- Use keyword-only arguments (
*in the signature). - Always include
**kwargsand pass them tosuper().__init__(**kwargs). - Never assume you know every argument — other classes in the chain may need their own.
Django’s Cooperative Views
Django’s class-based views are a textbook example of cooperative inheritance:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView
class ProtectedArticleList(LoginRequiredMixin, ListView):
model = Article
template_name = "articles/list.html"
login_url = "/login/"
The MRO is: ProtectedArticleList → LoginRequiredMixin → ListView → ... → View → object.
When a request arrives, dispatch() flows through the chain:
LoginRequiredMixin.dispatchchecks authentication.- If authenticated,
super().dispatch()reachesListView.dispatch. ListView.dispatchroutes toget()orpost().
Debugging MRO Issues
Inspect the Chain
for cls in MyClass.__mro__:
if hasattr(cls, 'method') and 'method' in cls.__dict__:
print(f"{cls.__name__} defines method()")
Common Failures
Missing super() call:
class BrokenMixin:
def save(self):
print("BrokenMixin save")
# Forgot super().save() — chain stops here
Non-cooperative third-party class:
If a library class doesn’t call super(), you can’t use it in a cooperative chain. Wrap it in an adapter instead.
Inconsistent MRO:
class A: pass
class B(A): pass
class C(A, B): pass # TypeError — A before B contradicts B(A)
Fix: class C(B, A) — let the more specific class come first.
Performance
Cooperative inheritance has no meaningful runtime cost beyond regular method calls. The MRO is computed once at class definition time and cached. super() creates a lightweight proxy object — CPython optimizes this heavily (it’s a single C-level operation in most cases).
The real cost is cognitive: developers need to understand the MRO and cooperation contract. For teams unfamiliar with the pattern, composition may be more maintainable.
When to Prefer Composition
Use cooperative inheritance when:
- Behaviors must intercept/wrap the same method chain (like Django views).
- You need aspect-oriented patterns (logging, validation, caching wrapping the same operation).
- A framework mandates it (Django, SQLAlchemy).
Use composition when:
- Behaviors are independent (a
Userhas anAddress, not “is an Address”). - You need to swap implementations at runtime.
- The inheritance hierarchy would exceed 3-4 levels.
One thing to remember: Cooperative multiple inheritance is a protocol, not a language feature. super() provides the mechanism, but every class must uphold the contract — calling super() and forwarding unknown arguments. When everyone cooperates, you get elegant layered behavior. When one class defects, the chain breaks silently.
See Also
- Python Abc Abstract Base Classes Why Python's ABC module is like a building inspector who checks your blueprints before construction begins
- Python Class Decorators Understand Class Decorators through an everyday analogy so Python behavior feels intuitive, not random.
- Python Composition Vs Inheritance Understand Composition Vs Inheritance through an everyday analogy so Python behavior feels intuitive, not random.
- Python Dataclasses Advanced Understand Dataclasses Advanced through an everyday analogy so Python behavior feels intuitive, not random.
- Python Descriptors Understand Descriptors through an everyday analogy so Python behavior feels intuitive, not random.