A row class declares `__slots__`, yet its instances still accept a misspelled attribute. Why?
answer
- The declaration is per class, not per tree
- Look at the whole ancestry, not the leaf
- One slot-less ancestor reopens everything
- Empty tuple on the classes adding nothing
- Two storage-carrying bases will not combine
basics
~20 sSome class in the MRO does not declare __slots__, so it contributes an instance __dict__ that every subclass inherits. The declaration constrains only the class that writes it; one slot-less ancestor or subclass reopens the object.
solid answer
~40 s`__slots__` is a per-class declaration, not a hierarchy-wide mode. An instance gets a `__dict__` unless *every* class in its MRO — bases, mixins and the class itself — declares slots. So a base written as a plain `class Row:` with no declaration, a mixin that forgot one, or a subclass that simply omits it all reintroduce the dictionary, and arbitrary attributes are assignable again with no error anywhere. The fix is to declare `__slots__` on every class in the chain, using the empty tuple `__slots__ = ()` for the ones that add no fields of their own. Two related traps: listing `"__dict__"` explicitly re-enables the dictionary on purpose, and combining two bases that each declare a non-empty `__slots__` raises TypeError at class creation because their instance layouts cannot be merged.
code
python · 13 linesclass Base:
pass
class Row(Base):
__slots__ = ("amount",)
r = Row()
r.typo = 1
print(r.__dict__)
for cls in type(r).__mro__:
if cls is not object and "__slots__" not in cls.__dict__:
print("slot-less:", cls.__name__)go deeper
Remember the headline: the declaration only covers the class that writes it, so a parent or child without one brings the instance dictionary back and arbitrary attributes are accepted again.
Explain the MRO rule and the empty-tuple idiom, and know that combining two bases that each declare non-empty slots fails at class creation with a layout conflict.
Demonstrate the diagnosis on real code: read an instance's __dict__, walk the MRO for the slot-less class, and judge whether the fix is declaring the empty tuple everywhere or dropping the constraint honestly.
Own the tradeoff at hierarchy scale — the layout conflict caps how many storage-carrying bases you can compose, so decide whether a dictionary-free invariant is worth shaping the class tree around, and how it stays enforced.
## The rule, stated precisely An instance has a `__dict__` if **any** class in its MRO gives it one. `__slots__` is the opt-out, and it opts out only for the class that writes it. Miss one class anywhere in the chain and every instance below it is dictionary-backed again. That single rule generates every symptom people report. **A slot-less base.** This is the common one, because the base often predates the decision: ```python class Base: # no declaration pass class Row(Base): __slots__ = ("amount",) r = Row() r.amoount = 1 # accepted; lands in the inherited __dict__ ``` `Row` genuinely has its `amount` slot and its descriptor, and the slot is still where `amount` is stored — data-descriptor precedence sees to that. But the object also carries `Base`'s dictionary, so every *other* name is accepted, and the typo-catching that motivated the declaration is gone. **A subclass that omits it.** The same failure, one generation later. `class Detailed(Row): pass` has no declaration of its own, so its instances get a dictionary even though `Row` was careful. **A mixin.** Mixins are the sneakiest case: a small `class LoggableMixin:` with two methods and no `__slots__` is enough to reopen every class that mixes it in. **`object` itself is fine.** Inheriting directly from `object` costs nothing — `object` gives its instances no dictionary. Only intermediate Python classes do. ## What a diagnosis looks like Take a nightly payment reconciliation job that loads a 6,800-row batch into a slotted row class. Rows normalise fine most nights, but the run that hits an encoding mismatch takes a retry path that sets `row.raw_payload` on the way through — a field nobody declared. Nothing raises. The retry records stash a second copy of every failed row's bytes, the batch's footprint climbs, and the "slots keep this bounded" assumption in the design doc turns out never to have held. The check is direct: build one instance and ask for its `__dict__`. If it answers instead of raising, something in the MRO is slot-less, and you find it by walking `type(obj).__mro__` and looking for a class whose own namespace has no `__slots__` entry — remembering that `__slots__` is not inherited as a summary, so you must test each class's own attributes, not just read the leaf's tuple. ```python for cls in type(row).__mro__: if cls is not object and "__slots__" not in cls.__dict__: print("slot-less:", cls.__name__) ``` The remedy is mechanical: declare `__slots__ = ()` on every class in the chain that adds no fields of its own. The empty tuple is the whole point of the idiom — it declares "I add no storage" while still opting out of the dictionary. ## The multiple-inheritance limit Slots reserve fixed storage in the instance layout, and two independent layouts cannot be merged. Deriving from two bases that each declare a *non-empty* `__slots__` raises `TypeError: multiple bases have instance lay-out conflict` when the class is created. There is no workaround at the language level; you restructure — put the storage on one base and keep the other a behaviour-only mixin declaring `__slots__ = ()`, which combines freely because it adds no layout. This is a real constraint on how far a slotted hierarchy can grow, and it is the honest argument against pushing slots through a deep class tree with several storage-carrying mixins. ## The deliberate escape hatch Listing `"__dict__"` inside the tuple gives instances both the fixed slots and a dictionary. It is a legitimate move when a framework or a third-party mixin needs to stash attributes on your objects, but in review it deserves a question: it restores exactly the behaviour the declaration was there to prevent, and it is often a quick silencing of an AttributeError whose real cause was a caller setting an undeclared field. ## Version note These inheritance rules are unchanged through Python 3.14. The one modern convenience worth knowing is that the standard-library dataclass decorator takes a `slots=True` option, which builds the declaration for you from the field list — useful, but it does not change any of the rules above, and the ancestors still have to cooperate.
- Why do you write `__slots__ = ()` on a class that adds no attributes of its own?Because omitting the declaration is not neutral — it gives the class's instances a `__dict__`, and every subclass inherits it, undoing the constraint for the whole branch. The empty tuple says "I add no storage" while still opting out of the dictionary. It is the standard idiom for mixins, abstract bases and thin subclasses in a hierarchy that means to stay dictionary-free.
- Why does inheriting from two classes that each declare non-empty slots raise TypeError?Each non-empty declaration reserves fixed storage in the instance layout, and two independently laid-out bases cannot be merged into one object. The error is `multiple bases have instance lay-out conflict`, raised at class creation. There is no flag to bypass it: you restructure so that at most one base carries storage and the others declare `__slots__ = ()`, which adds no layout and therefore combines freely.
- How do you check at runtime whether instances of a class really have no dictionary?Instantiate one and read its `__dict__`: it raises AttributeError when there is genuinely none. To find the culprit when there is one, walk `type(obj).__mro__` and report any class other than `object` whose own namespace lacks a `__slots__` entry — checking each class's own attributes, since the leaf's tuple lists only what that class declared and says nothing about its ancestors.
Declaring slots is like sealing one deck of a ship: water still gets everywhere if any deck above or below was left open.
saying these in an interview costs you the question
- Thinks declaring slots on the leaf class covers the whole hierarchy
- Forgets that a slot-less mixin reintroduces the dictionary
- Says inheriting from object itself brings back a __dict__
- Claims a subclass automatically inherits the slots-only behaviour
- Suggests listing __dict__ in the tuple as the fix for an AttributeError
- Believes two slotted bases can be combined with enough care