PythonMastery
reference 3 min read · lesson 30 of 45 in Errors

RuntimeError: dictionary changed size during iteration

1 · The lesson

read

What this error means

You are iterating over a dict (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 iteration

The 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 iteration

You 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 mutation

When 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.
  • RuntimeError: Set changed size during iteration — same root cause for sets.
  • RuntimeError: deque mutated during iteration — same for collections.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

Practice this

on practicepython.in

Short exercises that run in your browser and tell you what your code actually did, not just whether a test passed.