Why does functools.cached_property raise TypeError on a class that defines __slots__?
answer
- What does __slots__ take away?
- The descriptor needs somewhere to write
- Fails on first read, not at class creation
- TypeError names the class and the attribute
- Adding __dict__ to __slots__ undoes the saving
basics
~10 s__slots__ removes the per-instance __dict__, and that dictionary is exactly where cached_property writes its result. With nowhere to cache, the first read raises TypeError naming the class and the attribute.
solid answer
~50 s`functools.cached_property` caches by assigning into `instance.__dict__`. Declaring `__slots__` on a class tells CPython to give instances fixed storage slots and **no** `__dict__`, so the descriptor's storage target does not exist. On the first read it catches the `AttributeError` from touching `instance.__dict__` and re-raises a clear `TypeError: No '__dict__' attribute on 'Row' instance to cache 'size' property.` Crucially this fires **at access time, not at class creation**, so a slotted class with an unused cached attribute imports and constructs perfectly and only blows up on the code path that reads it. The fixes are to add `"__dict__"` to `__slots__` (which restores a dict and gives back most of what slots saved), to compute eagerly into a real slot, or to hand-roll a sentinel-guarded slot if the memory saving is the point of using slots at all.
code
python · 22 linesfrom functools import cached_property
class Row:
__slots__ = ("payload",)
def __init__(self, payload):
self.payload = payload
@cached_property
def size(self):
return len(self.payload)
try:
Row("abc").size
except TypeError as exc:
print(exc)
# No '__dict__' attribute on 'Row' instance to cache 'size' property.
class Fixed(Row):
__slots__ = ("__dict__",) # restores a per-instance dict
print(Fixed("abcd").size) # 4go deeper
Remember the pairing to avoid: a class with __slots__ has no per-instance dictionary, and cached_property needs one. If you see that TypeError, look at the class declaration first.
Explain the mechanism and the timing: the descriptor writes into instance.__dict__, slots remove it, and the TypeError fires on the first read rather than at class creation.
Show you have thought about which fix fits: restoring a dict, computing eagerly, or a sentinel-guarded slot behind a plain property — and why the memory motive for slots decides between them.
Own the guidance for a codebase: when slots are worth their rigidity at all, and how you keep a lazily-failing combination like this out of rarely-exercised code paths through review or static checks.
## Two mechanisms with directly opposed goals `__slots__` exists to remove the per-instance dictionary. A normal instance carries a `__dict__` that can hold any attribute name; a slotted class instead declares its attribute names up front, and CPython lays them out as fixed offsets in the object, like struct fields. The payoff is smaller instances and slightly faster attribute access, which matters when a program holds a great many small objects — a chat-transcript archiver materialising a few million message records, say. `functools.cached_property` exists to write a computed value into that very dictionary. Its `__get__` does, in essence: ```python try: cache = instance.__dict__ except AttributeError: raise TypeError( f"No '__dict__' attribute on {type(instance).__name__!r} " f"instance to cache {self.attrname!r} property." ) from None ``` So the two features are not merely awkward together; one deletes the thing the other requires. The error is deliberate and specific — it names the class and the attribute rather than letting a raw `AttributeError` escape from inside the descriptor, because a bare `AttributeError` raised during attribute access is easily mistaken for "the attribute does not exist". ## The failure is lazy, which is the real trap Nothing is checked when the class is created. `__set_name__` runs, records the attribute name and is happy; the class imports, instances construct, and every other attribute works. The `TypeError` arrives the first time some code path actually reads the cached attribute. In practice that means a slotted class with a rarely-exercised cached attribute can pass a 340-case regression pack and still fail in production on the one branch nobody covered — and the failure surfaces mid-operation, so if that read sits inside a partial-failure rollback path, the rollback itself is what explodes. Treat "slots plus cached_property" as a static-analysis concern, not something tests will reliably catch for you. ## The fixes, and what each costs **Add `"__dict__"` to `__slots__`.** This is legal and restores a per-instance dictionary while keeping the declared slots as real slots: ```python class Row: __slots__ = ("payload", "__dict__") ``` It is one line and it works, but understand what you bought: the instance now carries a dictionary again, so most of the memory saving that motivated `__slots__` is gone. You keep the typo protection for the declared names only partially — arbitrary attributes can be set again, because the dict is back. If slots were added for discipline rather than memory, this is a reasonable trade; if they were added because you have millions of instances, it defeats the point. **Compute eagerly into a real slot.** If the value is always needed and not ruinously expensive, do it in `__init__` and store it in a declared slot. You lose laziness, you gain predictable cost and no descriptor at all. This is usually the right answer for slotted classes, because a class with slots is normally a class you are creating in bulk, and bulk creation with a lazily-cached expensive field is an unusual shape. **Hand-roll the lazy slot.** Declare a slot for the cache and a sentinel: ```python _MISSING = object() class Row: __slots__ = ("payload", "_size") def __init__(self, payload): self.payload = payload self._size = _MISSING @property def size(self): if self._size is _MISSING: self._size = len(self.payload) return self._size ``` This preserves both laziness and the memory win, at the cost of a `property` that runs its getter on every read — a data descriptor call rather than a dict hit. That is a small, constant, well-understood cost, and it is the standard pattern when both properties matter. ## Two adjacent facts worth knowing **Inheritance can rescue you accidentally.** A subclass that does not declare `__slots__` gets a `__dict__` automatically, so a slotted base with a `cached_property` may work fine through one subclass and fail through a sibling that declares its own slots. If you are debugging a `TypeError` that appears for some instances of an apparent hierarchy and not others, check which classes in the MRO declare slots. **A read-only mapping fails too, differently.** If `instance.__dict__` exists but does not support item assignment, the descriptor raises a second, distinct `TypeError` saying the dictionary does not support item assignment for caching that property. That is what you see if you try to hang a `cached_property` off something whose namespace is a read-only mapping proxy — a class object being the common case, since a class's `__dict__` is a mapping proxy rather than a plain dict. The one-sentence answer an interviewer wants: **`cached_property` needs a writable instance dictionary, `__slots__` removes it, and the mismatch is reported as a `TypeError` on first read rather than at class definition.**
- Why is it a TypeError rather than an AttributeError?The descriptor catches the `AttributeError` it gets from touching `instance.__dict__` and re-raises a `TypeError` naming the class and the attribute. A bare `AttributeError` escaping an attribute read would read as "this attribute does not exist" and send you hunting for a typo, when the real problem is that the class shape cannot support caching at all.
- A slotted base class with a cached attribute works through one subclass and fails through another. Why?A subclass that does not declare `__slots__` gets a per-instance `__dict__` automatically, so instances of it can cache; a sibling that declares its own `__slots__` stays dictionary-free and raises. When debugging this, walk the MRO and check which classes declare slots rather than looking at the base alone.
- If you add "__dict__" to __slots__, what have you actually kept?The declared names still resolve through their slots, so those specific attributes keep the faster fixed-offset access. What you lose is the memory saving and the closed attribute set — the instance carries a dictionary again and arbitrary names can be assigned. If slots were adopted for footprint on millions of objects, this fix cancels the reason you adopted them.
saying these in an interview costs you the question
- Expecting the error at class definition rather than first read
- Claiming slots and cached_property are simply incompatible with no workaround
- Thinking the value gets stored in a slot automatically
- Saying the error is an AttributeError about a missing attribute
- Adding __dict__ to __slots__ without noting the memory saving is gone