How does weakref.WeakValueDictionary differ from a plain dict holding the same values?
answer
- A container can own what it stores
- Which side of the entry is weak?
- Storing it must not keep it alive
- Entry leaves with the last strong name
- len shrinks by itself, lookup raises KeyError
basics
~20 sA plain dict keeps a strong reference to every value it stores, so nothing inside it can be collected. weakref.WeakValueDictionary stores weak references instead: when the last strong reference elsewhere goes away, the entry removes itself.
solid answer
~40 sBoth map keys to objects; they differ in ownership. A `dict` holds a strong reference to each value, so putting an object in one guarantees it lives as long as the dict does — which is how an unbounded cache becomes a leak. `weakref.WeakValueDictionary` stores a weak reference to each value: it hands the object back while someone else still holds it, and the moment that last strong reference disappears the entry is dropped, so `len()` shrinks on its own and `cache[key]` raises `KeyError`. Keys are still held strongly; only the value side is weak. The practical consequence is that it never extends an object's lifetime — it remembers objects that are alive anyway, which makes it a deduplicating identity map rather than a retention policy.
code
python · 15 linesimport weakref
class Transcript:
def __init__(self, key):
self.key = key
cache = weakref.WeakValueDictionary()
held = Transcript("2026-09-03/room-7")
cache["room-7"] = held
print(len(cache), cache["room-7"].key)
del held # last strong reference goes away
print(len(cache), "room-7" in cache)go deeper
Recall the one-line difference: a dict owns its values, a weakref.WeakValueDictionary does not. Be able to say that an entry disappears by itself once the last strong reference elsewhere is gone.
Explain the mechanics: values are stored as weak references with a removal callback, keys stay strong, and lookup raises KeyError after collection. Know why check-then-fetch is a race and why .get() plus a local name is the safe shape.
Demonstrate judgement about when weakness is right at all. Show that a weak mapping supplies deduplication, not retention, and that a real cache needs a bounded strong tier beside it with an explicit size.
Own the framing: which layer of the system is the owner of an object's lifetime, and how that ownership is made visible. Weak containers push the decision elsewhere, so the tradeoff you argue is where retention policy and its memory ceiling actually live.
### Two mappings, one difference: who owns the object A `dict` is an *owning* container. Every value you put into one is reachable from the dict, so the interpreter must keep that object alive for at least as long as the dict itself is alive. That is usually what you want, and it is also the single most common way a Python process grows without bound: a module-level dictionary used as a cache is, by construction, a list of things that may never be freed. `weakref.WeakValueDictionary` changes exactly one thing about that arrangement. Internally each value is stored not as the object itself but as a weak reference to it — a reference that lets you reach the object while it is alive but that is deliberately *not counted* when the interpreter decides whether the object is still needed. Keys are stored normally, strongly. So the mapping can still answer `cache["room-7"]` with the real object, but it never prolongs that object's life. ### What "the entry disappears" actually means When the last strong reference to a stored value goes away, the weak reference held by the mapping is cleared, and a callback attached to that weak reference removes the whole key/value entry from the mapping. Three observable consequences follow, and interviewers usually probe at least one of them: * `len(cache)` shrinks on its own, with no code of yours having deleted anything. * `cache[key]` raises `KeyError`, exactly as if the key had never been inserted; `cache.get(key)` returns the default instead. * A key that was `in` the mapping a moment ago may not be a moment later, so the check-then-fetch pattern (`if key in cache: use(cache[key])`) is a genuine race even in a single thread — the object can die between the two statements if the only thing keeping it alive was a temporary. The robust shape is `obj = cache.get(key)` followed by `if obj is not None:`, because binding the result to a local name creates a strong reference that holds the object for the rest of the block. On CPython, "the last strong reference goes away" is normally immediate, because the object's reference count drops to zero the instant you rebind or `del` the last name. If the object is tangled in a reference cycle, its count never reaches zero and the entry survives until the cycle collector runs, so eviction is prompt but not guaranteed-instant. ### The mental model that makes it click A `WeakValueDictionary` is not a cache in the usual sense, because a cache's job is to *retain*. This mapping retains nothing. What it does is guarantee that while an object is alive, everyone asking for the same key gets the *same* object rather than a duplicate. That makes it an interning or deduplication table — an identity registry over objects whose lifetime is decided somewhere else entirely. Canonical uses are: one shared parsed document per file path while any request still holds it; one connection wrapper per endpoint while a caller is using it; a registry of live listeners that must not stop them from being collected. Read the class name literally and the whole API follows: the **value** is weak. The keys are held strongly, so a mapping keyed by big objects still pins those keys — that is what `weakref.WeakKeyDictionary` is for, and `weakref.WeakSet` is the set-shaped member of the same family. ### Constraints you inherit Because the values are stored as weak references, every value must be *weakly referenceable*. Most user-defined class instances are, and so are functions, classes, modules, sets and generators, but many fixed-layout built-ins are not: `int`, `str`, `bytes`, `tuple`, `list` and `dict` all raise `TypeError` when you try to take a weak reference to them. Storing a plain string in a `WeakValueDictionary` therefore fails loudly at insertion time rather than misbehaving later, which is a mercy. A class that declares `__slots__` also loses weak-reference support unless `'__weakref__'` is one of the slots. ### Choosing between them Use a plain `dict` when you want the mapping to be the owner and you are prepared to bound it — an explicit eviction policy, a maximum size, or a short lifetime. Use a `WeakValueDictionary` when something else already owns the objects and you only need to find them again while they exist. The two compose well: a bounded strong structure decides *how many* recent objects stay alive, and a weak mapping alongside it guarantees that no key is ever represented by two different objects at once. That two-tier shape gives you a real hit rate and a memory ceiling at the same time, which neither container achieves alone. Finally, note the reverse failure mode. Because the mapping is self-pruning, it is easy to conclude "my cache works, memory is flat" when in fact the cache never retains anything and every lookup misses. Flat memory and a zero hit rate look identical from the outside; measure hits explicitly rather than inferring health from RSS.
- Can len() on a weakref.WeakValueDictionary over-count objects that are already dead?On 3.14 removal is atomic — the entry goes as soon as the referent's weak reference clears — so `len()` reflects the live entries at the instant you read it. It is still not a stable number: an object can die between reading the length and using it. Through 3.13 the implementation deferred removals during iteration and subtracted them from the reported length, which produced the same guarantee by a different route.
- If a stored value is part of a reference cycle, when does its entry disappear?Not when you drop the last external reference. Objects in a cycle keep each other's reference counts above zero, so the value survives until the cycle collector reclaims the group; only then does the weak reference clear and the entry vanish. Eviction from a weak mapping is prompt for acyclic objects and merely eventual for cyclic ones, which is why memory can fall in steps rather than smoothly.
- Why is `if key in cache: use(cache[key])` unsafe on a weakref.WeakValueDictionary?The membership test proves the value was alive a moment ago, not that it still is; if nothing else holds it, it can be collected between the two statements and the subscript then raises `KeyError`. Write `obj = cache.get(key)` and test `obj is not None` — binding the result to a local name creates a strong reference that keeps the object alive for the rest of the block.
A plain dict is a locker: whatever you put in stays because the locker holds it. A WeakValueDictionary is a coat-check ticket for a coat someone else is wearing — useful while they wear it, worthless the moment they leave.
saying these in an interview costs you the question
- Says a WeakValueDictionary keeps its values alive like a normal cache
- Thinks the keys are held weakly as well as the values
- Expects a missing weak value to return None from cache[key]
- Assumes len() cannot change unless you insert or delete
- Claims any object at all can be stored as a weak value