RuntimeError: dictionary changed size during iteration (tested fixes)

RuntimeError: dictionary changed size during iteration means a loop over a dict added or removed a key. We tested five kinds of change inside a loop and found two less obvious things: the change has already been made when the error appears, and some changes don't raise at all.

1. What triggers it, and what doesn't

We changed a dict five different ways inside a for k in d loop and printed the dict afterwards:

def attempt(label, fn):
    d = {"a": 1, "b": 2, "c": 3}
    try:
        fn(d); print(f"{label:30} OK -> {d}")
    except RuntimeError as e:
        print(f"{label:30} RuntimeError: {e} -> {d}")

def delete(d):
    for k in d:
        if d[k] == 2: del d[k]
def add(d):
    for k in d:
        d[k * 2] = 0
def pop_add(d):                       # size ends up the same
    for k in d:
        if k == "a": d.pop("c"); d["z"] = 9
def change_values(d):
    for k in d:
        d[k] *= 10
def set_existing(d):
    for k in d:
        d["a"] = 0

attempt("delete a key", delete)
attempt("add keys", add)
attempt("pop one, add one", pop_add)
attempt("change values", change_values)
attempt("assign to existing key", set_existing)

s = {1, 2, 3}
try:
    for x in s:
        s.discard(x)
except RuntimeError as e:
    print(f"{'set: discard while looping':30} RuntimeError: {e}")

Output:

delete a key                   RuntimeError: dictionary changed size during iteration -> {'a': 1, 'c': 3}
add keys                       RuntimeError: dictionary changed size during iteration -> {'a': 1, 'b': 2, 'c': 3, 'aa': 0}
pop one, add one               OK -> {'a': 1, 'b': 2, 'z': 9}
change values                  OK -> {'a': 10, 'b': 20, 'c': 30}
assign to existing key         OK -> {'a': 0, 'b': 2, 'c': 3}
set: discard while looping     RuntimeError: Set changed size during iteration

Three things to notice:

Sets have the same rule, with a capital S in the message: Set changed size during iteration.

2. Three fixes that work

d = {"a": 1, "b": 2, "c": 3, "d": 4}

for k in list(d):                     # 1. loop over a copy of the keys
    if d[k] % 2 == 0:
        del d[k]
print("list(d):        ", d)

d = {"a": 1, "b": 2, "c": 3, "d": 4}
d = {k: v for k, v in d.items() if v % 2}       # 2. build a new dict
print("comprehension:  ", d)

d = {"a": 1, "b": 2, "c": 3, "d": 4}
to_delete = [k for k, v in d.items() if v % 2 == 0]   # 3. collect, then delete
for k in to_delete:
    del d[k]
print("collect+delete: ", d)

Output:

list(d):         {'a': 1, 'c': 3}
comprehension:   {'a': 1, 'c': 3}
collect+delete:  {'a': 1, 'c': 3}

list(d) makes a copy of the keys first, so the loop no longer depends on the dict. The comprehension builds a new dict, which is the cleanest option when you're filtering. Rebinding d doesn't change other references to the old dict, so use d.clear(); d.update(new) if other code holds the same object. Collect-then-delete is easiest to read when the condition is complicated. The same list(...) fix works for sets (for x in list(s)). If the dict is shared between threads, a copy isn't enough on its own: another thread can still change it while list(d) is copying, so protect it with a lock.

Related: our dict has no attribute iteritems guide explains why Python 3's items() is a live view of the dict.

Frequently asked questions

How do I fix dictionary changed size during iteration?

Loop over a copy of the keys: for k in list(d). Or build a new dict with a comprehension, or collect the keys first and delete them after the loop.

Can I change dict values while iterating?

Yes. Changing values or assigning to existing keys does not change the size and raised no error in our test. Only adding or removing keys does.

Is the dict modified when the RuntimeError is raised?

Yes. The deletion or addition that triggered it had already been applied, so the dict is left partly processed.

Written with AI assistance. Measured on 2026-10-22 on Debian with Python 3.13.5: deleting, adding, swapping keys, changing values and reassigning existing keys inside a for loop over a dict, discarding from a set while looping, and three fixes. Every block was run verbatim and its output pasted unedited. PlainHeaders is our extension; Python is free.