PYTHON / OBJECT-ORIENTED PYTHON
Multiple inheritance and mixins
Combine several base classes deliberately, write reusable mixins, and order bases so their overrides and super() calls actually run.
What you will learn
- Place mixins to the left of the concrete base so their overrides come first in the MRO
- Write cooperative __init__ methods that accept **kwargs and forward through super()
- Read Cls.__mro__ to predict which definition of a method a call will reach
- Recognise the TypeError Python raises when a base order cannot be linearised
Understanding Multiple inheritance and mixins
A class statement can list more than one base: `class Point(Serializable, Comparable)`. Python does not search those bases as a tree at call time; it computes one flat, ordered list of classes once, at class creation, and stores it as `Point.__mro__`. Every attribute lookup walks that single list from left to right and stops at the first class that defines the name. The right mental model for multiple inheritance is therefore a queue of classes, not a branching hierarchy.
A mixin is a class written to sit somewhere in that queue and contribute exactly one slice of behaviour. It is intentionally incomplete: it usually has no state of its own, is never instantiated directly, and it freely calls methods or reads attributes it does not define, on the assumption that whatever class it is mixed into supplies them. `Serializable.to_dict` reading `vars(self)`, or a caching mixin calling `super().get(key)`, are both mixins leaning on their host. The `Mixin` name suffix is pure convention; Python treats these as ordinary classes.
Two rules make mixins work in practice. First, order: because lookup goes left to right, a mixin that means to override or wrap a base method must be listed before that base, otherwise the base's plain version is found first and the mixin is silently bypassed. Second, cooperation: if a mixin defines `__init__`, it must call `super().__init__(...)` and pass along the keyword arguments it does not consume, because `super()` inside a mixin does not mean "my parent" but "the next class in the MRO of the actual instance" - which is how a mixin inheriting only from `object` can still forward into a base class it has never heard of.
class Serializable:
def to_dict(self):
return {k: v for k, v in vars(self).items() if not k.startswith("_")}
class Comparable:
def __eq__(self, other):
if type(self) is not type(other):
return NotImplemented
return self.to_dict() == other.to_dict()
class Point(Serializable, Comparable):
def __init__(self, x, y):
self.x = x
self.y = y
self._dirty = False
p = Point(1, 2)
print(p.to_dict())
print(p == Point(1, 2), p == Point(3, 4))
print([c.__name__ for c in Point.__mro__])A mixin works because Python flattens all bases into one linear MRO, so the mixin sits in front of the concrete base and its super() calls forward into it.
Worked examples
Cooperative __init__ across mixins
Each class consumes the keyword arguments it owns and forwards the rest, so one super() chain initialises every layer.
class LoggedMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.log = []
def record(self, message):
self.log.append(message)
class Timestamped:
def __init__(self, created="unknown", **kwargs):
super().__init__(**kwargs)
self.created = created
class Note(LoggedMixin, Timestamped):
def __init__(self, text, **kwargs):
super().__init__(**kwargs)
self.text = text
n = Note("buy milk", created="2024-05-09")
n.record("created")
print(n.text, n.created, n.log)
print([c.__name__ for c in Note.__mro__])Example explained
Line 1Note.__init__ keeps `text` and passes `created` onward untouched inside **kwargs.
Line 2LoggedMixin declares no parameters of its own, so it forwards everything and only then sets self.log.
Line 3Timestamped pulls `created` out of the keyword arguments and calls object.__init__() with an empty set, which is the only reason no TypeError is raised.
Line 4The MRO shows LoggedMixin before Timestamped, which is the exact order in which the super() chain unwinds.
Mixin order decides whether it runs
The same two classes combined in the opposite order produce a class where the caching wrapper is never reached.
class Repository:
def get(self, key):
return "db:" + key
class CachingMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._cache = {}
def get(self, key):
if key not in self._cache:
print("miss", key)
self._cache[key] = super().get(key)
else:
print("hit", key)
return self._cache[key]
class CachedRepo(CachingMixin, Repository):
pass
class WrongOrder(Repository, CachingMixin):
pass
r = CachedRepo()
print(r.get("a"))
print(r.get("a"))
print(WrongOrder().get("a"))Example explained
Line 1In CachedRepo the MRO is CachedRepo, CachingMixin, Repository, so `get` resolves to the mixin.
Line 2`super().get(key)` inside the mixin reaches Repository.get even though CachingMixin inherits only from object.
Line 3In WrongOrder, Repository comes first, so Repository.get is found and the mixin's version is never consulted - no 'miss' line is printed.
Line 4WrongOrder still gets `_cache` from the mixin's __init__, which shows the object can be half-wired without any error.
An impossible base order
Listing a parent before its own subclass makes linearisation impossible and fails at class creation time.
class Base:
pass
class Middle(Base):
pass
try:
class Broken(Base, Middle):
pass
except TypeError as exc:
print("TypeError:", exc)
class Works(Middle, Base):
pass
print([c.__name__ for c in Works.__mro__])Example explained
Line 1`class Broken(Base, Middle)` asks for Base before Middle, but Middle is a subclass of Base and must precede it.
Line 2The TypeError is raised while executing the class statement, not when an instance is created.
Line 3Reversing the bases to (Middle, Base) satisfies both constraints, and Base appears once in the resulting MRO.
Important notes
A mixin that defines __eq__ without __hash__ has its __hash__ set to None, and every class mixing it in becomes unhashable - define __hash__ in the mixin if instances must go into sets or dict keys.
Class attributes declared on a mixin are shared by every class that mixes it in, so a counter or registry stored there will not be per-subclass unless you key it by type(self).
Common mistakes
Writing `class Job(Record, LoggingMixin)` instead of `class Job(LoggingMixin, Record)`: Record's method is found first, the mixin's override never executes, and there is no error to point at the problem.
Calling `Base.__init__(self, ...)` by name inside a mixin instead of `super().__init__(...)`: any class sitting between them in the MRO is skipped, so its attributes are missing and you get an AttributeError later, far from the cause.
Omitting **kwargs forwarding in a mixin's __init__: keyword arguments meant for a later class arrive at object.__init__ and raise `TypeError: object.__init__() takes exactly one argument`.
Try it yourself
Change, predict, then run
Write a `ValidatedMixin` whose `save()` prints "validating" and then calls `super().save()`, and a `Record` class whose `save()` prints "saved"; create `class Job(ValidatedMixin, Record)` and call `Job().save()`, then swap the two bases and note which line disappears from the output.
Open the Python workspaceCheck your understanding
CachingMixin.get caches results and delegates with super().get(key). You define `class CachedRepo(Repository, CachingMixin)` and find that calls to get() never hit the cache and raise no error. What explains it?
- CachingMixin.get must be decorated so Python knows it replaces a base method.
- super().get() inside a mixin only resolves if the mixin itself subclasses Repository.
- Repository appears before CachingMixin in the MRO, so lookup finds Repository.get first.
- Python inherits methods only from the first base class listed.
Show answer
Attribute lookup walks the MRO left to right and stops at the first class defining the name; with Repository listed first, its plain get shadows the mixin's. Option 2 is tempting but wrong: super() follows the MRO of type(self), not the mixin's own bases, which is exactly why a mixin inheriting only from object can delegate into Repository once the order is fixed.