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 stops with an error, so that it doesn't silently skip or repeat keys.
When you see it
Traceback (most recent call last):
File "clean.py", line 4, in <module>
for key in scores:
RuntimeError: dictionary changed size during iterationThe minimal reproduction:
scores = {"alice": 92, "bob": 41, "carol": 88, "dave": 33}
for name in scores:
if scores[name] < 50:
del scores[name] # crashes 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. Changing the value of an existing key is fine. Only adding or removing keys triggers this error.How to fix it
Option 1: iterate over a snapshot. This is the smallest fix: one extra function call.
for name in list(scores): # list() copies the keys once if scores[name] < 50: del scores[name]
setup added so this can run · re-runs 1 earlier block on this page first
# 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') # Earlier blocks on this page that this one builds on, re-run quietly first. _pm_earlier = [ 'scores = {"alice": 92, "bob": 41, "carol": 88, "dave": 33}\n\nfor name in scores:\n if scores[name] < 50:\n del scores[name] # crashes on the next iteration', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
list(scores), tuple(scores), and list(scores.items()) all work. The snapshot is independent of the live dict.
Option 2: collect first, then mutate. This is cleaner when the deletion logic is complex.
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 · re-runs 2 earlier blocks on this page first
# 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') # Earlier blocks on this page that this one builds on, re-run quietly first. _pm_earlier = [ 'scores = {"alice": 92, "bob": 41, "carol": 88, "dave": 33}\n\nfor name in scores:\n if scores[name] < 50:\n del scores[name] # crashes on the next iteration', 'for name in list(scores): # list() copies the keys once\n if scores[name] < 50:\n del scores[name]', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Option 3: build a new dict instead of mutating the old one. This is the most idiomatic option, and nothing changes in place.
scores = {name: score for name, score in scores.items() if score >= 50}This is also the right choice when you transform the values as well as filter them:
scores = {name: score * 1.1 for name, score in scores.items()}Option 4: for sets, use the same patterns.
seen = {1, 2, 3, 4}
seen -= {x for x in seen if x % 2 == 0} # set difference: no set changes while a loop runs over itWhen you'd 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. Here the RuntimeError helps you: 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 wrong results with no warning, 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 did, as well as whether a test passed.
Found this useful? Share it with a friend who’s learning Python.