reference
3 min read
·
lesson 30 of 45 in Errors
RuntimeError: dictionary changed size during iteration
1 · The lesson
readWhat this error means
You are iterating over adict (or a set, which raises the same kind of error) and you added or removed a key during the loop. CPython's dict iterator records the dict's size when iteration starts and checks it on every __next__. If the size has changed, it bails out rather than silently skip or revisit keys.
When you see it
text
Traceback (most recent call last):
File "clean.py", line 4, in <module>
for key in scores:
RuntimeError: dictionary changed size during iterationThe minimal reproduction:
python
scores = {"alice": 92, "bob": 41, "carol": 88, "dave": 33}
for name in scores:
if scores[name] < 50:
del scores[name] # boom on the next iterationYou can also trigger it with pop, popitem, update adding new keys, or clear.
Why it happens
Python's dict iterator is a view over the underlying hash table. Adding a key can trigger a resize, which moves entries to new slots; deleting a key leaves a tombstone. Either way the iterator's position is no longer meaningful. Instead of returning wrong results, CPython raises. Note that changing the value for an existing key is fine — only structural changes (adding or removing keys) trigger this.How to fix it
Option 1 — iterate over a snapshot. Cheapest fix, two characters.
python
for name in list(scores): # list() copies the keys once if scores[name] < 50: del scores[name]
setup added so this can run · defines scores
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) scores = _AutoMock('scores')
list(scores), tuple(scores), and list(scores.items()) all work. The snapshot is independent of the live dict.
Option 2 — collect first, mutate after. Cleaner when the deletion logic is complex.
python
to_remove = [name for name, score in scores.items() if score < 50] for name in to_remove: del scores[name]
setup added so this can run · defines scores
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) scores = _AutoMock('scores')
Option 3 — build a new dict instead of mutating. Most idiomatic, no in-place trickery.
python
scores = {name: score for name, score in scores.items() if score >= 50}This is also the right move when you have a transformation, not just a filter:
python
scores = {name: score * 1.1 for name, score in scores.items()}Option 4 — for sets, the same patterns apply.
python
seen = {1, 2, 3, 4}
seen -= {x for x in seen if x % 2 == 0} # set difference, no iteration mutationWhen you'd actually see this in real code
- A cache eviction pass that iterates entries and deletes expired ones in place.
- A "deduplicate and normalise" loop that pops bad keys as it walks the dict.
- A graph traversal that prunes nodes from the adjacency dict while visiting neighbours.
- Concurrent code: a thread mutates a dict while another iterates it. The RuntimeError is a gift here — the real bug is the missing lock.
Related errors
RuntimeError: Set changed size during iteration— same root cause for sets.RuntimeError: deque mutated during iteration— same forcollections.deque.- For lists, you don't get an error — you get silently wrong results, which is worse. Use the snapshot pattern there too.
See Also
- All Python errors — the full index, by type and by when it happens.
- Dicts and sets — mutation patterns.
- Comprehensions — building new collections instead of mutating.
- cheat-collections — fast lookup.
Practice this
on practicepython.inShort exercises that run in your browser and tell you what your code actually did, not just whether a test passed.