PythonMastery
reference 3 min read · lesson 30 of 60 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 stops with an error, so that it doesn't silently skip or repeat 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]      # crashes 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. 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.

python
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.

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 · 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.

python
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:

python
scores = {name: score * 1.1 for name, score in scores.items()}

Option 4: for sets, use the same patterns.

python
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 it

When 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.
  • 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 wrong results with no warning, 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 did, as well as whether a test passed.

Found this useful? Share it with a friend who’s learning Python.