weakref: WeakValueDictionary Iteration Silently Skips GC'd Entries (tested)
Mutating a plain dict's size while iterating over it is a well-known mistake that Python catches for you -- it raises RuntimeError: dictionary changed size during iteration. A weakref.WeakValueDictionary can shrink mid-iteration too, but not because your code called del on it directly -- it shrinks because something else in the program dropped the last reference to one of its values. We measured whether that gets the same protection.
1. A plain dict catches you. A WeakValueDictionary doesn't.
d = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e"}
try:
for k in d:
if k == 1:
del d[3] # mutate size during iteration
except RuntimeError as e:
print("plain dict:", type(e).__name__, e)
plain dict: RuntimeError dictionary changed size during iteration
import weakref
class Obj:
def __init__(self, n): self.n = n
wd = weakref.WeakValueDictionary()
objs = {i: Obj(i) for i in range(5)}
for i, o in objs.items():
wd[i] = o
seen = []
for k in wd: # no try/except -- nothing is raised
seen.append(k)
if k == 1:
del objs[3] # drop the last strong ref to value 3, NOT a dict mutation
print("keys visited:", seen)
print("final keys: ", list(wd.keys()))
keys visited: [0, 1, 2, 4]
final keys: [0, 1, 2, 4]
Measured: key 3 silently never appears in the iteration, and the loop never raises anything -- across repeated runs, deterministically. We never called del wd[3]; we only dropped the last strong reference to the value stored at that key, from a completely different variable (objs), and the dictionary shrank anyway, invisibly, in the middle of a loop that had no idea anything outside it could do that.
2. len() stays internally consistent -- which makes the vanish harder to notice
import weakref
class Obj:
def __init__(self, n): self.n = n
wd = weakref.WeakValueDictionary()
objs = {i: Obj(i) for i in range(5)}
for i, o in objs.items():
wd[i] = o
print("before:", len(wd))
for k in wd:
if k == 1:
del objs[3]
print("after: ", len(wd))
before: 5
after: 4
Measured: len(wd) correctly drops from 5 to 4 -- the dictionary isn't corrupted or left in some inconsistent state, and nothing crashes later either. That's actually what makes this easy to miss in a real program: there's no exception to catch, no wrong count to notice, just one fewer iteration than you expected, with the cause living in unrelated code that happened to drop a reference at the wrong moment.
Frequently asked questions
Does iterating a WeakValueDictionary raise RuntimeError if its size changes mid-loop, like a plain dict does?
No -- we measured dropping the last strong reference to a not-yet-visited value during iteration: the key silently never appears in the loop, and nothing is raised, across repeated deterministic runs.
Does a WeakValueDictionary have to be mutated directly (like del wd[key]) to shrink during iteration?
No -- we measured it shrinking because a completely separate variable (objs[3]) dropped the last strong reference to the value, with no call to del on the dictionary itself and no code in the loop touching that key.
Does len() reveal that an entry vanished during iteration?
It reflects it correctly (5 before, 4 after in our measurement) but gives no signal that it happened mid-loop or which key was affected -- there's no exception and no inconsistent count to flag the event.
How is this different from the documented caution about WeakValueDictionary and GC?
The docs note that entries can vanish due to garbage collection during iteration; what we measured and confirmed concretely is that this specific case -- a plain, deterministic drop of the last reference, not even requiring a GC cycle -- produces silent skipping with zero exception, in contrast to the RuntimeError a plain dict gives for the equivalent direct mutation.
Written with AI assistance. Measured on 2026-10-08 with Python 3.13.5's built-in weakref module: iterated a WeakValueDictionary, dropped the last strong reference to one of its not-yet-visited values mid-loop, and compared the result (and any exception) against doing the equivalent size-changing mutation to a plain dict during iteration. PlainHeaders is our extension; Python's weakref module is built in.