Skip to main content

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.

Next up in your learning path