Why does `weakref.ref()` raise TypeError on an instance of a class declaring `__slots__`?
answer
- Slots drop more than the dictionary
- A second hidden field also disappears
- Weak caches break far away
- One extra name in the tuple restores it
- It may be declared only once per hierarchy
basics
~10 sWeak-reference support needs its own storage cell, which a slotted class only gets if "__weakref__" is listed in the declaration. Without it the type cannot hold weak references and weakref.ref() refuses the object.
solid answer
~40 sAn ordinary class gives instances two hidden pieces of storage: the `__dict__` and the list of weak references to the object. Declaring `__slots__` opts out of both. Since the weak-reference cell is gone, `weakref.ref(obj)` raises `TypeError: cannot create weak reference to ... object`, and anything built on weak references — a weak-value cache, a `weakref.WeakSet`, a finalizer — fails the same way. The fix is to list `"__weakref__"` alongside your own names in the tuple, which restores exactly that one cell without bringing back the dictionary. The slot may appear only once in a hierarchy: re-declaring it in a subclass whose base already has it raises TypeError at class creation, and a slot-less base anywhere in the MRO already supplies it, in which case you must not declare it again.
code
python · 15 linesimport weakref
class NoRef:
__slots__ = ("value",)
class WithRef:
__slots__ = ("value", "__weakref__")
try:
weakref.ref(NoRef())
except TypeError as exc:
print(exc)
w = WithRef()
print(weakref.ref(w)() is w)go deeper
Recall that a declared-slots class cannot be referred to weakly unless the weak-reference name is listed in the tuple alongside your own attribute names.
Explain why: weak references need a per-instance cell to hold the references pointing at the object, and the declaration reserves that cell only when you name it.
Show that you would recognise the symptom from the far end — a weak cache or finalizer raising TypeError after an unrelated class gained a slots declaration — and know the once-per-hierarchy rule when fixing it.
Treat it as an API decision: whether your objects are meant to participate in weak caches and registries at all, and whether the TypeError should stay as a deliberate signal rather than be papered over.
## Two hidden fields, not one The mental model most people carry is "slots removes the instance dictionary". That is half of it. A plain heap class gives its instances *two* implicit pieces of storage: 1. the per-instance `__dict__`, and 2. a cell holding the weak references that point at the object. `__slots__` opts out of both, because both are storage the class layout would otherwise reserve. Dropping the dictionary is the effect everyone knows; dropping weak-reference support is the one that surfaces later, from somewhere far away in the codebase. ```python import weakref class NoRef: __slots__ = ("value",) weakref.ref(NoRef()) # TypeError: cannot create weak reference to 'NoRef' object ``` ## Why weak references need storage at all A weak reference must be *cleared* when its target is collected, so every live weak reference has to be reachable from the target itself — the object keeps a list of the references aimed at it, and the deallocator walks that list and blanks each one before the memory goes. That list needs a place to live in the instance, and a type that reserved no cell for it cannot support the operation at all. Hence the failure is a `TypeError` about the *type*, raised eagerly, rather than a per-object runtime error. ## The fix, and its one rule List the name explicitly: ```python class WithRef: __slots__ = ("value", "__weakref__") ``` That restores the single cell and nothing else — instances still have no `__dict__` and still reject undeclared names. It is a normal entry in the tuple, so it interacts with inheritance the same way storage does, with one extra constraint: **the cell may exist only once in a hierarchy.** Declaring `"__weakref__"` in a subclass whose base already has it raises `TypeError: __weakref__ slot disallowed: we already got one` at class creation. Declare it on the base that first needs it, and never again below. The corollary catches people the other way round: any slot-less class in the MRO already supplies weak-reference support along with the dictionary, so a slotted class inheriting from a plain base supports `weakref.ref` without declaring anything — and must not declare it, or the class will not build. ## Where it actually bites The failure never appears where the class is defined. It appears the day someone puts your objects into a structure that holds them weakly: a `weakref.WeakValueDictionary` used as an identity cache, a `weakref.WeakSet` of live listeners, an observer registry that must not keep its subscribers alive, or a `weakref.finalize` cleanup hook. The traceback names `weakref`, so people go looking at the cache, when the change that broke it was a declaration added to a data class three modules away for unrelated reasons. The same asymmetry explains a puzzling pair: an old dictionary-backed class works in the cache, a new slotted one raises, and diffing the two classes shows nothing but `__slots__`. ## Deciding what to do about it There are only three honest options. Declare `"__weakref__"` if the objects are meant to be referenced weakly — the cost is one pointer per instance and no loss of the name discipline. Do not declare it if they are not, and let the TypeError document that intent: it is a genuine signal that someone is using the objects in a way the class did not anticipate. Or reconsider the declaration entirely if the objects turn out to be long-lived, registry-managed things rather than the small, numerous records that motivate slots in the first place. What you should not do is add `"__dict__"` to the tuple hoping it fixes the weak-reference error. The two cells are independent: restoring the dictionary does not restore weak-reference support, and you would give up the name discipline for nothing. ## Version note This behaviour, the exact slot name, and the once-per-hierarchy rule are unchanged through Python 3.14.
- Does adding `"__dict__"` to the tuple also restore weak-reference support?No. The dictionary cell and the weak-reference cell are independent pieces of instance storage, and each is restored only by naming it. Adding `"__dict__"` gives back arbitrary attribute assignment — surrendering the whole point of the declaration — while `weakref.ref` still raises TypeError. If you need weak references, name the weak-reference slot; if you need both, name both.
- A slotted class inherits from a plain class with no declaration. Can its instances be referenced weakly?Yes. A slot-less class in the MRO already reserves both the dictionary and the weak-reference cell, and every subclass inherits them. In that case you must *not* declare the weak-reference slot yourself: the cell already exists, and re-declaring it raises TypeError at class creation. It also means the branch has a `__dict__`, so the name discipline is already gone.
- Which standard-library constructs will fail against a slotted class that omits the weak-reference slot?Anything that holds the object weakly: `weakref.ref` and `weakref.proxy` directly, and on top of them `weakref.WeakValueDictionary`, `weakref.WeakSet`, `weakref.WeakKeyDictionary` when such an object is the key, and `weakref.finalize` cleanup hooks. All raise TypeError naming the type, which is why the traceback usually points at the cache or registry rather than at the class that changed.
A weak reference is a forwarding note that must be torn up when the object dies, and the object has to keep the list of who to notify; a slotted class with no room reserved for that list simply cannot be referred to weakly.
saying these in an interview costs you the question
- Thinks slots only remove the instance dictionary
- Suggests adding __dict__ to fix a weak-reference TypeError
- Declares the weak-reference slot again in every subclass
- Believes weak references work on any Python object
- Blames the weak cache instead of the class declaration