Backend / Python core / Tricky questions / 38_dict_mutation_during_iteration.md

Mutating a dict during iteration

Updated 3 min read source
On this page8
  1. The gotcha
  2. Minimal repro
  3. Why it happens
  4. How to avoid
  5. What about popitem?
  6. What about dict.items() and views?
  7. Concurrent modification across threads
  8. Interview angle

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

python
d = {1: 'a', 2: 'b', 3: 'c'}

for k in d:
    if k == 2:
        del d[k]            # RuntimeError: dictionary changed size during iteration
python
# Re-assigning values is allowed:
for k in d:
    d[k] = d[k].upper()      # OK — size unchanged
python
# Adding keys also raises:
for k in list(d):
    # OK because we iterate over a list snapshot
    d[k * 10] = "new"

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):

python
s = {1, 2, 3}
for x in s:
    # RuntimeError: Set changed size during iteration
    s.add(4)

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

python
for k in list(d):
    if k == 2:
        # OK — iterating over a copy of keys
        del d[k]

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:

python
d = {k: v for k, v in d.items() if k != 2}

Two-pass: collect, then delete

python
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?

python
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?

python
items = d.items()
# dict_items([(1, 'a'), (2, 'b'), (3, 'c')])
print(items)

del d[1]
# dict_items([(2, 'b'), (3, 'c')])   ← view is dynamic
print(items)

d.keys(), d.values(), d.items() return views, not snapshots. They reflect later changes. So:

python
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 4

  • 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, dict internals.