skip to content

Why does `weakref.ref()` raise TypeError on an instance of a class declaring `__slots__`?

level: seniorimportance: nice to knowfreq 15%

answer

  1. Slots drop more than the dictionary
  2. A second hidden field also disappears
  3. Weak caches break far away
  4. One extra name in the tuple restores it
  5. It may be declared only once per hierarchy

basics

~10 s

Weak-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 s

An 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 lines
python
import 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context