When do you stop adding mixins to a Python class and delegate to a collaborator object instead?
answer
- Weigh the costs, not the purity
- One shared namespace across every base
- Contracts a mixin never declares
- Bases are fixed at class creation
- A mixin that grew an __init__
basics
~20 sWhen the shared behaviour needs its own state or lifecycle, when mixins start reaching for attributes they do not create, or when their method names begin to collide. Delegation costs forwarding code but gives each piece an independent, swappable object.
solid answer
~50 sMixins are cheap while they stay small, stateless and few: no forwarding code, and the capability appears directly on the instance. They stop being cheap at three specific thresholds. First, state — every base shares one instance namespace, so two mixins that both keep a `_cache` collide with nothing to warn them. Second, undeclared contracts — a mixin reading `self.session` it never created is coupled to a host it cannot name. Third, variability — bases are fixed when the class is created, so a mixin cannot be swapped per instance, per environment or per test, while a collaborator passed into `__init__` can. I keep mixins for stateless cross-cutting behaviour used by several unrelated classes, and delegate whenever the behaviour has configuration, a lifecycle, or more than one implementation. The tell that it should have been an object is usually a mixin that grew an `__init__`.
code
python · 18 linesclass Loader:
def fetch(self, sample_id):
return {"id": sample_id}
class CachingLoader:
def __init__(self, source):
self._source = source
self._seen = {}
def fetch(self, sample_id):
if sample_id not in self._seen:
self._seen[sample_id] = self._source.fetch(sample_id)
return self._seen[sample_id]
loader = CachingLoader(Loader())
print(loader.fetch("S-1200"), loader.fetch("S-1200"))go deeper
Know that inheritance is not the only way to share behaviour: a class can simply hold another object and call it. Be able to sketch both versions of a small caching example.
Explain the mechanics behind the choice — one shared instance namespace across all bases, bases fixed when the class is created, and the forwarding code delegation actually costs.
Show the judgement with evidence: which of state, undeclared contracts or the need to swap implementations pushed a real design from a mixin to a collaborator, and what the migration cost.
Own the policy for a codebase: where mixins are allowed, how their host contracts are declared, when a capability graduates to an object, and how you keep the combined vocabulary reviewable as the team grows.
## Both mechanisms share behaviour; they differ in what they cost - A **mixin** adds capability by inheritance: the methods appear on the instance, there is no forwarding code, and callers see one flat surface. - A **collaborator** adds capability by holding another object and calling it: construction becomes explicit, the piece is separately testable and replaceable, and someone has to write the forwarding. The judgement is not about which is purer — it is about which cost this codebase can afford where. ## Threshold one: shared namespace Every base of a class writes into the same `self`. Two mixins from two teams that each keep `_cache`, `_retries` or `_log` silently overwrite one another, and neither file mentions the other, so no reviewer of either change can see the collision. There is no namespacing mechanism to fall back on: unlike a collaborator, whose state lives behind its own attribute, mixin state is pooled by construction. Prefix conventions and name mangling reduce accidents but do not make the pooling go away. ## Threshold two: undeclared contracts A mixin that reads `self.session` or calls `self.emit()` without defining either has a contract with its host that exists only in its author's head. It works until someone mixes it into a class that has no `session`, or into one whose `emit` means something else. A collaborator has to be handed its dependencies through its own `__init__`, so the contract is written down where the object is constructed, and a missing piece fails at wiring time instead of on an unusual code path in production. ## Threshold three: variability Bases are fixed when the class object is created. That makes a mixin the wrong tool whenever the behaviour must vary: - a different implementation per environment, - a null implementation in tests, - two policies chosen at startup. Achieving that with inheritance means building classes dynamically or carrying a flag inside the mixin, both of which are worse than the collaborator that simply gets passed a different object. If the answer to *can this be swapped* is yes, it is an object. ## The recurring tell A mixin that grows an `__init__` has usually outgrown the pattern. Now every host must cooperate in construction and pass the right arguments through the chain, which means the class that wanted one capability has taken on an ordering obligation. At that point the behaviour has its own state and its own lifecycle, which is the definition of a thing rather than a trait. Two more tells: - a mixin used by exactly one class is just code in the wrong file, - and a mixin that can only be tested by composing an increasingly elaborate host class is telling you how big its hidden contract really is. ## What delegation costs, honestly Forwarding methods are real work and real noise, and reaching for a catch-all attribute-forwarding hook to avoid writing them is a blunt instrument: it forwards names you never intended, confuses tooling and introspection, and turns a typo into a silent miss. Delegation also moves the capability off the instance's own surface, so callers write `loader.cache.stats()` instead of `loader.stats()`, and existing call sites have to change. Where the behaviour is genuinely small, stateless and universal, paying that cost buys very little — which is why the pattern is worth keeping rather than abolishing. ## A workable policy - **Mixins** for small, stateless, cross-cutting capability wanted by several classes that share no other ancestor, with the required host contract declared as abstract methods rather than assumed, and a cap on how many any one class carries — past three or four, nobody can predict the combined behaviour from the class statement alone. - **Collaborators** for anything with state, configuration, a lifecycle, more than one implementation, or a need to be replaced in a test. - The middle case, a capability wanted by many classes *and* carrying state, is best served by a thin mixin that holds no state itself and simply forwards to a collaborator the host constructs — the flat call surface without the pooled namespace. ## The organisational angle a lead owns A wide mixin vocabulary is a shared language: every mixin multiplies the combinations reviewers must reason about, and the interactions are invisible at every call site because they live in a bases tuple. Collaborators scale differently — more objects, more wiring, but each one comprehensible alone. Which cost dominates depends on how many people touch these classes and how confidently they can be tested, and that judgement, not a slogan, is what an interviewer is listening for.
- How do you keep the flat call surface of a mixin while avoiding pooled state?Use a thin mixin that holds no state itself and forwards to a collaborator the host constructs. The capability still appears directly on the instance, but its state lives behind one attribute the collaborator owns, and the collaborator remains independently testable and replaceable. It is more moving parts than either option alone, and worth it when many classes want the capability and the capability has state.
- Is catch-all attribute forwarding a reasonable way to avoid writing forwarding methods?Rarely. It forwards every name you did not think about, so a typo becomes a silent miss rather than an error, and it hides the real surface from readers, editors and type checkers. Explicit forwarding methods are more typing but document the contract exactly. Reserve the catch-all for genuine proxy or wrapper objects where forwarding everything is the actual intent.
- How many mixins on one class is too many?There is no number in the language, but the practical limit is comprehension: past three or four, nobody can predict the combined behaviour from the class statement, and each addition multiplies the interactions a reviewer must consider. Treat a growing count as a design signal rather than a rule violation, and look for the ones carrying state — those are the candidates to turn into collaborators first.
saying these in an interview costs you the question
- Argues from a slogan instead of the concrete costs
- Forgets that all bases share one instance namespace
- Thinks a mixin can be swapped per instance at runtime
- Adds an __init__ to a mixin without seeing the signal
- Uses catch-all attribute forwarding to skip writing methods
- Denies that delegation costs any forwarding code at all