Python RuntimeError: dictionary changed size during iteration
Fix RuntimeError when mutating a dict or set while iterating it, and choose between iterating a copy, over list(d), or deferred deletion.
Symptoms
- RuntimeError: dictionary changed size during iteration
- RuntimeError: Set changed size during iteration
- The error appears only when a filter branch deletes a key
Possible causes
- Code deletes or adds keys inside a for key in my_dict loop
- A callback invoked during iteration mutates the same mapping
- set.discard / set.add called while iterating the set
- A defaultdict grows lazily when a missing key is read during iteration
Diagnosis steps
Locate the loop and the mutation
Interpretation
Look for del, pop, assignment, or update inside a for loop over the same object.
Reproduce minimally
python -c "d={1:1,2:2}; [d.pop(k) for k in d]"Interpretation
Confirming the pattern reproduces the error shows the mutation is the cause.
Check for implicit growth
Interpretation
Reading a missing key from a defaultdict inserts it, which also changes size mid-loop.
Root cause
Python dict and set iterators are live views. Changing the container size during iteration invalidates the iterator and raises RuntimeError to prevent undefined traversal. Even an implicit insert from a defaultdict or a callback can trigger it.
Fix
Iterate over a snapshot: `for k in list(d):` or `for k, v in d.copy().items():`. Or collect keys to delete and remove them after the loop with a dict comprehension. Use `d.pop(k, None)` outside the loop, and avoid defaultdict reads inside iteration.
Verify the fix
Re-run the routine; no RuntimeError occurs and the container has the expected final contents, verified by an assertion on its size and keys.
Symptoms
A loop over a dict or set raises RuntimeError: dictionary changed size during iteration or its Set variant, usually only on the branch that mutates the container.
Diagnosis
Find the mutation inside the loop:
for key in my_dict:
if not wanted(key):
del my_dict[key] # raises RuntimeError
Minimal reproductions with pop confirm the pattern, and reads from a defaultdict can insert keys implicitly.
Fix
Iterate a snapshot, or defer deletions:
for key in list(my_dict):
if not wanted(key):
del my_dict[key]
# or build a new dict
my_dict = {k: v for k, v in my_dict.items() if wanted(k)}
Verification
Re-run and assert the final keys and size. The error disappears and the container reflects the intended deletions.
Patterns That Hide the Mutation
The mutation is not always a literal del. A helper called inside the loop that pops from the container, a defaultdict read that inserts a default, or a generator that yields while another coroutine mutates the same dict all trigger the same RuntimeError. When the loop body is short but the error persists, search the call graph of every function invoked inside the loop for any write to the container, then move those writes outside the iteration or snapshot the keys first.