Python Multiple Inheritance & Mixins
OOP Architecture
Multiple Inheritance & Mixins
Python allows a class to inherit from multiple bases. Learn when to embrace it and how to avoid the dreaded diamond problem.
Two parents, one class
List multiple bases in the class definition and the child inherits from all of them, left to right:
class Serializer:
def to_dict(self):
return vars(self)
class Comparable:
def __eq__(self, other):
return vars(self) == vars(other)
class Point(Serializer, Comparable):
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.to_dict()) # {'x': 1, 'y': 2}
print(p == Point(1, 2)) # True
When both parents define the same method, the leftmost base wins — the exact order is the method resolution order, which you can always inspect with Point.__mro__.
The diamond problem
The classic hazard: two parents share a grandparent. If each parent called the grandparent's __init__ directly, it would run twice. Python's super() solves this — it doesn't call "the parent", it calls the next class in the MRO, so each __init__ runs exactly once:
class A:
def __init__(self):
print("A.__init__")
class B(A):
def __init__(self):
print("B.__init__")
super().__init__()
class C(A):
def __init__(self):
print("C.__init__")
super().__init__()
class D(B, C):
def __init__(self):
print("D.__init__")
super().__init__()
D()
# D.__init__
# B.__init__
# C.__init__ <- B's super() called C, its sibling — not A
# A.__init__ <- runs once, at the end
The MRO for D is D → B → C → A → object. super() in B forwards to C because that's what comes next for a D instance — this is why every class in a cooperative hierarchy should call super().__init__(), even ones that inherit only from object.
Mixins
The legitimate, everyday use of multiple inheritance is mixins:
- Lightweight classes that provide reusable methods but no state.
- Naming convention:
SomethingMixin. - Example:
from datetime import datetime, timezone
class BaseModel:
def save(self):
print(f"saving {type(self).__name__}")
class TimestampMixin:
def touch(self):
self.created_at = datetime.now(timezone.utc)
class AuditedModel(TimestampMixin, BaseModel):
...
m = AuditedModel()
m.touch()
m.save() # saving AuditedModel
print(m.created_at.tzinfo) # UTC
The mixin isn't a base class in any meaningful sense — nobody instantiates TimestampMixin on its own. It's a bag of behavior you bolt onto a real hierarchy. Mixins go before the base class in the bases list so their overrides take precedence.
Cooperative super() with **kwargs
When several __init__ methods each take their own arguments, the standard pattern is: consume what you need, forward the rest with **kwargs:
class Named:
def __init__(self, name, **kwargs):
self.name = name
super().__init__(**kwargs)
class Aged:
def __init__(self, age, **kwargs):
self.age = age
super().__init__(**kwargs)
class Person(Named, Aged):
pass
p = Person(name="Ada", age=36)
print(p.name, p.age) # Ada 36
Named.__init__ peels off name and passes age=36 along the chain to Aged, which passes an empty **kwargs on to object. Every class stays reusable in any position of the MRO, because none of them hardcodes who comes next.
When to avoid it
- If both parents carry real state and overlapping responsibilities, the hierarchy becomes hard to reason about — prefer composition.
- If you can't describe each extra base as "adds one focused capability", it shouldn't be a mixin.
- Keep mixins focused on a single behavior.
- Ensure cooperative
super()calls so each parent runs. - Document expected attributes/methods that mixins rely on.
Frequently Asked Questions
Which parent's method wins when both define it?
The one that appears first in the method resolution order — for simple cases, the leftmost base in the class definition. Python computes the full order with the C3 linearization algorithm; inspect it with ClassName.__mro__ and see the MRO guide at /oop/python-mro for the details.
class X:
def hello(self): return 'X'
class Y:
def hello(self): return 'Y'
class Z(X, Y): ...
print(Z().hello()) # X
Why does the shared grandparent __init__ only run once in a diamond?
Because super() follows the MRO, not the class tree. In D(B, C), the order is D, B, C, A — so B's super() call goes to C (its sibling), and only C's super() reaches A. Each class appears once in the MRO, so each __init__ runs once, provided every class in the chain calls super().__init__().
What's the difference between a mixin and a base class?
A base class models what the object is and typically owns state and an __init__. A mixin adds one capability (serialization, timestamps, logging), holds no state of its own, and is never instantiated directly. Practically: your class has one real base plus zero or more mixins listed before it.