skip to content

How does a class declaring `__slots__` store attribute values with no instance `__dict__`?

level: middleimportance: should knowfreq 32%

answer

  1. Storage is decided at class creation
  2. Something lives on the class, not the instance
  3. It is the descriptor protocol, nothing new
  4. Data descriptors outrank an instance dictionary
  5. Its type is called member_descriptor

basics

~10 s

Each name in __slots__ becomes a class-level descriptor bound to a fixed storage cell in the instance's memory layout. Attribute access runs that descriptor's get, set and delete hooks instead of any dictionary lookup.

solid answer

~50 s

When the type is built, every name listed in `__slots__` is turned into one descriptor object stored on the *class* — its type is named `member_descriptor` — and each descriptor knows a fixed offset into the instance's memory. `obj.x = 1` finds the data descriptor on the type and calls its set hook, which writes the reference into that cell; reading calls the get hook, which raises AttributeError when the cell is empty; `del obj.x` calls the delete hook and empties it again. Because the descriptors are data descriptors they take priority over an instance dictionary even when one exists. This also explains two rules that otherwise look arbitrary: a class variable of the same name is rejected, because it would have to occupy the same class-namespace key as the descriptor, and a subclass that re-declares an inherited slot name creates a *second*, shadowing descriptor over separate storage.

code

python · 12 lines
python
class Row:
    __slots__ = ("amount",)

print(type(Row.amount).__name__)
r = Row()
r.amount = 42
print(Row.amount.__get__(r, Row))
del r.amount
try:
    r.amount
except AttributeError as exc:
    print("unset slot:", exc)

go deeper

for a junior

Recall the one-line mechanism: the declared names become attributes handled by the class rather than entries in a per-instance dictionary, so lookup no longer goes through a mapping.

for a middle

Explain the descriptor protocol here: one data descriptor per name on the class, a fixed cell per instance, and get/set/delete hooks that raise AttributeError when the cell is unset.

for a senior

Show the consequences you have had to debug: data-descriptor precedence over any inherited dictionary, the silent shadowing when a subclass repeats an inherited name, and pairing a private slot with a validating property.

for a principal

Be ready to argue where this level of layout control is worth constraining a hierarchy at all, and how you keep such a convention enforceable — review rules or a base class — rather than tribal knowledge.

## Attribute lookup, and where slots sit in it For a normal instance, `obj.x` runs `object.__getattribute__`, which searches the type and its MRO first, then falls back to the instance's `__dict__`. The type search matters because of *descriptor* precedence: an object found on the class that defines `__set__` or `__delete__` is a **data descriptor**, and it wins over anything in the instance dictionary. `property` is the familiar example. `__slots__` reuses exactly that machinery. It adds no new lookup rule — it just installs one data descriptor per declared name: ```python class Row: __slots__ = ("amount",) print(type(Row.amount).__name__) # member_descriptor ``` `Row.amount` is a real object living on the class. Its type is called `member_descriptor`, and it carries the offset of one fixed cell inside every `Row` instance. That cell holds a reference to the stored value, exactly as a dictionary slot would, but its position is decided once, at class creation, instead of being looked up by hashing a string on every access. ## What each operation does * `r.amount = 42` → `object.__setattr__` finds the data descriptor on the type and calls its set hook, which stores the reference in the cell. * `r.amount` → the get hook reads the cell. If the cell is empty, it raises `AttributeError` naming the attribute — indistinguishable, from the caller's side, from an attribute that was never declared. * `del r.amount` → the delete hook empties the cell and drops the reference. A subsequent read raises again. Because the descriptor is on the class, you can drive it explicitly: `Row.amount.__get__(r, Row)` and `Row.amount.__set__(r, 42)` do exactly what the attribute syntax does. That is only a debugging party trick, but it is the clearest demonstration that nothing magical is happening — slots are ordinary descriptor protocol over fixed storage. ## The rules this explains **A same-named class variable is a ValueError.** The descriptor must be stored in the class namespace under the declared name. A class body that also assigns that name would overwrite it, so class creation refuses the collision outright rather than silently dropping one of them. **Re-declaring an inherited slot creates a second one.** If a base declares `("x",)` and a subclass declares `("x",)` again, the subclass gets its own descriptor over its own cell, and it shadows the parent's on lookup. The parent's cell still exists and is now unreachable through normal attribute access — it is wasted storage plus a real correctness trap, because a base-class method written against the parent descriptor and a subclass method written against the child descriptor read two different cells. The rule is simple: never repeat a name your base already declared. **Data-descriptor precedence still applies when a `__dict__` exists.** If a slot-less base in the MRO reintroduces an instance dictionary, the slot descriptors do not stop working, and they do not lose. Assignment still goes into the slot cell, because a data descriptor found on the type outranks the instance dictionary. Only names *not* covered by a descriptor land in the dictionary. **Properties and slots coexist, but not under the same name.** Both are data descriptors on the class, so `property` behaves normally on a slotted class — a common pattern is a private slot `("_amount",)` plus a public `property` named `amount` that validates on the way in. ## What it does not give you The descriptor stores a plain object reference. It performs no type check, no coercion and no validation; declaring a slot constrains the *set of names*, never the values. If you want checked values you write a property or your own descriptor class on top. It also does not make attribute access thread-safe or atomic beyond what a normal attribute assignment already is, and it does not change how `getattr`, `setattr` and `hasattr` behave — those route through the same `__getattribute__` path and therefore through the same descriptors. ## Version note This mechanism has been stable for the whole Python 3 line and is unchanged in 3.14; the descriptor type name and the offset-based storage are CPython implementation details that other implementations may realise differently while keeping the same observable semantics.

  • What goes wrong when a subclass re-declares a slot name its base already declared?
    The subclass gets a second descriptor over its own storage cell, shadowing the base's. The base's cell still exists but is unreachable through normal attribute access, so it is wasted space, and any base-class method that was compiled against the parent descriptor reads a different cell from the subclass's code. It is silent — no error is raised — so the discipline is simply never to repeat an inherited name.
  • If an instance somehow has both slots and a `__dict__`, where does `obj.x = 1` store the value?
    In the slot. Slot descriptors define both get and set, which makes them data descriptors, and a data descriptor found on the type takes precedence over the instance dictionary in `object.__getattribute__` and `object.__setattr__`. Only names with no descriptor land in the dictionary. This is the same precedence rule that makes `property` win over an instance attribute of the same name.
  • Can you use `property` on a class that declares `__slots__`?
    Yes, as long as the property and the slot do not share a name — sharing one is the class-variable collision that raises ValueError. The idiomatic pairing is a private slot such as `_cents` holding the value and a public property that validates or converts on the way in and out. Both are data descriptors on the class, so they compose without any special handling.

A dictionary-backed instance is a coat-check where every item is found by reading the name on its ticket; a slotted instance is a row of numbered pigeonholes, with the numbering fixed when the room was built.

saying these in an interview costs you the question

  • Says slots are stored in a hidden per-instance dictionary
  • Cannot say the storage lives on the instance and the descriptor on the class
  • Thinks slots enforce types or validate assigned values
  • Claims an instance dictionary would take precedence over a slot
  • Believes a subclass re-declaring a slot name simply reuses the base's storage

context