How should a cooperative __init__ chain use super() and **kwargs across several bases?
answer
- Everyone must keep the same shape
- Keyword-only, plus forward the rest
- Something at the end accepts nothing
- A leftover keyword becomes a TypeError
- One class opting out truncates the chain
basics
~20 sEvery initializer in the chain takes keyword-only parameters plus **kwargs, consumes what it owns, and passes the rest on with super().init(**kwargs). The chain ends at object.init, which accepts nothing, so a leftover keyword raises TypeError.
solid answer
~40 sThe contract is uniform: each `__init__` declares the keyword-only parameters it owns, forwards everything else with `super().__init__(**kwargs)`, and never assumes it is last in the chain. Positional arguments do not survive a chain whose order depends on the receiver, so cooperative initializers are keyword-only. The chain terminates at `object.__init__`, which takes no extra arguments, and that is a feature: a misspelled keyword nobody consumed reaches it and raises `TypeError: object.__init__() takes exactly one argument`, turning a silent typo into a startup failure. The rule that actually breaks in production is participation — one class that calls a base by name instead of `super()` truncates the chain, and the initializers after it never run. Because most classes create their state defensively, that shows up as wrong behaviour later, not as an error at construction.
code
python · 33 linesfrom collections import deque
class Bidder:
def __init__(self, *, budget, **kwargs):
super().__init__(**kwargs)
self.budget = budget
def record(self, bid):
if not hasattr(self, "history"):
self.history = []
self.history.append(bid)
class BoundedHistory:
def __init__(self, *, history_size=1000, **kwargs):
super().__init__(**kwargs)
self.history = deque(maxlen=history_size)
class Cooperative(BoundedHistory, Bidder):
def __init__(self, **kwargs):
super().__init__(**kwargs)
class Truncated(BoundedHistory, Bidder):
def __init__(self, **kwargs):
Bidder.__init__(self, **kwargs)
good = Cooperative(budget=100, history_size=2)
bad = Truncated(budget=100)
for i in range(5):
good.record(i)
bad.record(i)
print(type(good.history).__name__, list(good.history))
print(type(bad.history).__name__, bad.history)go deeper
Know that a subclass initializer normally starts with super().init() and that forgetting it means the base class never sets up its own attributes. The keyword forwarding pattern comes later.
Write the shape from memory: keyword-only parameters you own, **kwargs for the rest, one unconditional super().init(**kwargs). Explain why object.init rejecting extra keywords is useful rather than annoying.
Diagnose the truncated chain. Show how a direct base call, or swallowed kwargs, silently removes an initializer, why defensive lazy defaults hide it until it shows up as wrong behaviour or unbounded memory, and what test would have caught it at construction time.
Judge whether the hierarchy should exist. Cooperative construction is a contract every future subclass must honour, so weigh it against composition or a single documented base, and decide what the codebase enforces in review and in lint.
### The shape every participant has A cooperative initializer chain is a convention, not a language feature, and every class in the chain has to keep the same shape: ```python class Bidder: def __init__(self, *, budget, **kwargs): super().__init__(**kwargs) self.budget = budget ``` Three things are load-bearing. The parameters this class owns are **keyword-only** (after the bare `*`). Everything it does not own is collected in `**kwargs` and **forwarded** to `super().__init__`. And the call is made unconditionally, even though `Bidder` inherits from nothing but `object` — because the class after `Bidder` in some future receiver's order is not `object`, and `Bidder` cannot know who it will be. ### Why keyword-only, and why the chain ends where it does Positional arguments cannot survive this pattern. A class in the middle of the order has no way to know how many leading positional arguments belong to it rather than to whoever comes next, so any mismatch silently shifts values into the wrong parameters. Keywords are self-describing, so each class can take exactly what it recognises and pass the remainder untouched. The terminator is `object.__init__`, which accepts no arguments beyond the instance. That is the quality gate of the whole design: if every class consumed what it owns, the dictionary arriving at `object` is empty and construction succeeds. If a caller passes `history_size=2` but the class that owns it spells the parameter `historysize`, nobody consumes it, it reaches the end, and you get `TypeError: object.__init__() takes exactly one argument (the instance to initialize)`. A cooperative chain therefore validates its own keyword surface for free. ### The failure that actually happens Consider an ad-auction bidder assembled from two behaviour classes: one owns the budget, another installs a bounded history of recent bids. Someone refactoring the concrete class replaces `super().__init__(**kwargs)` with a direct `Bidder.__init__(self, **kwargs)` — it looks equivalent, and the tests pass: ```python class BoundedHistory: def __init__(self, *, history_size=1000, **kwargs): super().__init__(**kwargs) self.history = deque(maxlen=history_size) class Truncated(BoundedHistory, Bidder): def __init__(self, **kwargs): Bidder.__init__(self, **kwargs) # chain cut here ``` `BoundedHistory.__init__` now never runs, so `self.history` is never the bounded structure. Nothing raises, because `Bidder.record` was written defensively and lazily creates a plain list when the attribute is missing. The bidder runs, records every bid it ever sees, and the working set climbs past 2.4 GB over a day of traffic before anyone notices. The bug is not the memory growth; it is a skipped initializer, and the defensive default is what hid it. ### Rules that keep the chain intact 1. **Every** class in the hierarchy calls `super().__init__(...)` exactly once, including the ones whose only base is `object`. A class that opts out becomes a wall. 2. Call `super().__init__(**kwargs)` **before** using state that a later class in the order sets up, and after it when your own setup must precede theirs. Pick one order per hierarchy and document it. 3. Never name a base class directly in an initializer of a class designed for composition. The review rule is mechanical and easy to lint for. 4. Prefer defaults on the keyword parameters over defensive lazy attribute creation. A missing attribute that raises `AttributeError` at construction is a far better outcome than one silently invented at first use. 5. Test the composition, not just each class: construct the concrete class and assert that state from *every* participating initializer is present. That test fails the moment someone cuts the chain. ### When to stop This pattern earns its complexity only when several independent behaviour classes really do need to contribute construction state and are combined in more than one way. If there is exactly one combination, an explicit single-inheritance base with a plain signature is easier to read and impossible to truncate; composition — holding a history object as an attribute and passing it in — removes the problem entirely. Reach for a cooperative chain when composition genuinely costs more than it saves, not by default.
- Should super().__init__ be called first or last inside a cooperative initializer?Either, but consistently, and the choice is a documented part of the hierarchy's contract. Calling it first means the rest of the chain has finished before your own setup runs, which is what you want if your setup reads state others install. Calling it last is right when the classes after you depend on attributes you create. Mixing the two across one hierarchy produces ordering bugs that only appear for certain combinations.
- How do you catch a truncated chain in tests rather than in production?Construct the concrete class and assert on state contributed by every participating initializer, not just the one you are changing. A stronger variant has each participant append its own class to a list attribute during construction, and the test asserts that list equals the expected order; it fails immediately when someone replaces a super() call with a direct base call or forgets to call up at all.
- What happens if a class in the chain accepts **kwargs but does not forward them?Everything after it in the order is skipped exactly as if it had never called up, and the diagnostic that would have caught a typo disappears too: unconsumed keywords are swallowed instead of reaching object.__init__ and raising. Swallowing kwargs is worse than not calling super at all, because it removes the failure signal as well as the behaviour.
It is a bucket brigade: each class takes the pails addressed to it and passes the rest along. One person who sets their bucket down instead of handing it on ends the line, and the people behind them never receive anything.
saying these in an interview costs you the question
- Uses positional arguments through a cooperative chain
- Skips super().__init__ when the base is only object
- Swallows **kwargs instead of forwarding them upward
- Calls a named base initializer inside a composable class
- Creates missing state lazily, hiding a skipped initializer
- Thinks each base initializer runs automatically