Mutating a dict during iteration
The gotcha
Adding or removing keys while iterating raises RuntimeError: dictionary changed size during iteration. Reassigning values is fine. The distinction trips people up — and so does the workaround for delete-while-iterating.
Minimal repro
d = {1: 'a', 2: 'b', 3: 'c'}
for k in d:
if k == 2:
del d[k] # RuntimeError: dictionary changed size during iteration
# Re-assigning values is allowed:
for k in d:
d[k] = d[k].upper() # OK — size unchanged
# Adding keys also raises:
for k in list(d):
d[k * 10] = "new" # OK because we iterate over a list snapshot
Why it happens
Dict iteration relies on a stable internal layout. The dict tracks a version counter that increments on size change. The iterator captures this counter at start; on each __next__, it compares — mismatch raises.
Value reassignment doesn’t change size, so the version counter doesn’t tick. Iteration continues safely.
set and list have the same rule (with different error messages):
s = {1, 2, 3}
for x in s:
s.add(4) # RuntimeError: Set changed size during iteration
l = [1, 2, 3]
for x in l:
l.append(4) # silent infinite loop
# — list iterator advances by index, the loop never ends
(List iteration doesn’t raise — it just keeps going as the list grows. Different bug shape, same root cause.)
How to avoid
Iterate over a snapshot
for k in list(d):
if k == 2:
del d[k] # OK — iterating over a copy of keys
list(d) creates a one-time list of keys at iteration start. Mutating d during the loop doesn’t affect the list.
d.copy() does the same thing for items.
Build a new dict
Often cleaner than mutating in place:
d = {k: v for k, v in d.items() if k != 2}
Two-pass: collect, then delete
to_remove = [k for k, v in d.items() if some_condition(v)]
for k in to_remove:
del d[k]
Best when the predicate is non-trivial — separates iteration logic from mutation.
What about popitem?
while d:
k, v = d.popitem() # OK — not iterating
process(k, v)
Loop based on while d: rather than for k in d: — no iterator to invalidate.
What about dict.items() and views?
items = d.items()
print(items) # dict_items([(1, 'a'), (2, 'b'), (3, 'c')])
del d[1]
print(items) # dict_items([(2, 'b'), (3, 'c')]) ← view is dynamic
d.keys(), d.values(), d.items() return views, not snapshots. They reflect later changes. So:
for k in d.keys():
if cond:
del d[k] # RuntimeError, same reason
Use list(d.keys()) for a snapshot.
Concurrent modification across threads
The RuntimeError is single-thread defensive. With multiple threads modifying the same dict, you can get partial reads, dropped entries, or KeyError during lookup — none of it deterministic. Dict is not thread-safe; use threading.Lock or dict reads in one thread + writes in another via a queue.
Interview angle
- Q: “What happens if you delete a key while iterating?” —
RuntimeError: dictionary changed size during iteration. - Q: “What about reassigning values?” — fine; size unchanged.
- Follow-up: “How do you safely delete entries matching a predicate?” — iterate over
list(d), or build a new dict via comprehension. - Follow-up: “Does the same apply to lists?” — list mutation during iteration doesn’t raise; it silently misbehaves (infinite loop on append, skipped elements on delete).
See collection_complexity_bigO, 02_python_core/32_dict_internals.md.