Here is a situation that is easy to run into: you have a dictionary of scores and one simple job, remove everyone below 50. Four lines of code, nothing fancy. Python answers with this:
RuntimeError: dictionary changed size during iteration
Try the same pattern on a list and it doesn't crash at all. It quietly gives you the wrong answer. So why does the dictionary version scream while the list version stays silent?
Quick Summary
- In CPython, adding or removing keys while looping over a dictionary raises
RuntimeErroron the next step of the loop. - The crash can happen after the dictionary has already been partly modified, so you can be left with half-updated data.
- A list iterator can keep going after the list changes, which can cause elements to be skipped. A dictionary iterator detects the change and raises an error.
- Three safe fixes: loop over a snapshot of the keys, build a new dictionary, or collect the keys first and delete them afterwards.
What You Expect
scores = {"alice": 45, "sohel": 82, "carol": 30, "dave": 91}
for name, score in scores.items():
if score < 50:
del scores[name]
The logic reads fine: go through every student and delete the ones under 50. You'd expect alice and carol to disappear.
What Actually Happens
RuntimeError: dictionary changed size during iteration
The loop crashes. But look at the dictionary afterwards:
print(scores)
{'sohel': 82, 'carol': 30, 'dave': 91}
alice is already gone, but carol (30) is still there. The first deletion worked. The crash came on the next step of the loop, so the dictionary is now half-cleaned. If your code catches that error and carries on, you are working with data that is neither the original nor the result you wanted.
Why Lists Skip and Dictionaries Crash
I ran into the list version of this when deleting items from a list while looping. A list iterator advances through the list by position. When an element is removed, the later elements shift left, but the iterator keeps advancing, which can cause elements to be skipped.
Here is the same idea with a small list:
nums = [2, 4, 6, 8]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums)
[4, 8]
Every number was even, so the result should be an empty list. Instead, 4 and 8 survived, and Python said nothing.
A dictionary iterator behaves differently. In CPython, it detects that the dictionary has changed while it is being iterated and raises a RuntimeError. The Python documentation describes this as something that may happen when you add or delete entries during iteration, so treat the error as a warning you can rely on in CPython, not as a guarantee for every implementation.
So the two loops behave differently:
- List iterator: "Give me whatever is at the next index." It keeps going even if the list changed underneath it.
- Dictionary iterator: "The dictionary changed while I was iterating, so I can't safely continue." It stops.
That is arguably the better design. A crash is annoying, but a wrong answer that looks right is worse.
The Error Shows Up on the Next Step
Notice where the error appears. It is not raised on the line with del. It is raised when the loop asks for the next key. In my test on Python 3.12, even deleting on the very last iteration still fails:
d = {"a": 1, "b": 2}
for key in d:
if key == "b":
del d["b"]
RuntimeError: dictionary changed size during iteration
So a traceback pointing at the for line doesn't mean the for line is the problem. The change happened inside the loop body, on the previous iteration.
Adding Keys Breaks It Too
The same thing happens with insertions:
d = {"a": 1, "b": 2}
for key in d:
d[key + "_copy"] = d[key]
RuntimeError: dictionary changed size during iteration
Swapping Keys Doesn't Sneak Past It
You might wonder what happens if the size stays the same, for example, if you delete one key and add another in the same iteration:
d = {"a": 1, "b": 2, "c": 3}
for key in d:
if key == "a":
del d["a"]
d["z"] = 0
RuntimeError: dictionary keys changed during iteration
The message is different, but the result is the same. On Python 3.12, changing which keys are in the dictionary is enough to stop the loop, even if the count doesn't change.
Three Ways to Fix It
Fix 1: Loop over a snapshot of the keys
scores = {"alice": 45, "sohel": 82, "carol": 30, "dave": 91}
for name in list(scores):
if scores[name] < 50:
del scores[name]
print(scores)
{'sohel': 82, 'dave': 91}
list(scores) creates a separate list containing the dictionary's keys. The loop walks that list, so deleting keys from the original dictionary doesn't change what the loop is iterating over. This modifies the original dictionary in place, which matters if other code holds a reference to it.
Fix 2: Build a new dictionary
scores = {"alice": 45, "sohel": 82, "carol": 30, "dave": 91}
scores = {name: score for name, score in scores.items() if score >= 50}
print(scores)
{'sohel': 82, 'dave': 91}
This is usually the cleanest option, because you describe what you want to keep instead of what you want to remove. There is one catch, and it is aliasing again. This creates a new dictionary and points the name scores at it.
Anything else that referred to the old dictionary still sees the old data:
scores = {"alice": 45, "sohel": 82, "carol": 30, "dave": 91}
alias = scores
scores = {name: s for name, s in scores.items() if s >= 50}
print(scores)
print(alias)
{'sohel': 82, 'dave': 91}
{'alice': 45, 'sohel': 82, 'carol': 30, 'dave': 91}
If another part of your program holds a reference to the original dictionary and needs to see the cleanup, use Fix 1 or Fix 3, which modify it in place.
Fix 3: Collect the keys first, delete afterwards
scores = {"alice": 45, "sohel": 82, "carol": 30, "dave": 91}
to_remove = [name for name, score in scores.items() if score < 50]
for name in to_remove:
del scores[name]
print(scores)
{'sohel': 82, 'dave': 91}
The first loop only reads. The second loop only deletes, and it iterates over a plain list, not over the dictionary. It is a bit longer than Fix 1, but it separates "decide what to remove" from "remove it," which helps when the removal logic is complicated.
What Is Safe While Looping
Changing the values of keys that already exist is fine, because the set of keys doesn't change:
scores = {"alice": 45, "sohel": 82, "carol": 30, "dave": 91}
for name in scores:
scores[name] += 5
print(scores)
{'alice': 50, 'sohel': 87, 'carol': 35, 'dave': 96}
So the rule is narrower than "never touch a dictionary inside its own loop." The precise rule is: don't add or remove keys while iterating over the dictionary itself.
The Mental Model to Remember
Both loop bugs come down to the same question: what is the loop actually walking over?
- A list iterator advances by position. If the list shifts under it, it can step over items without any error.
- A dictionary iterator detects when its keys change and raises
RuntimeError. - The safe pattern in both cases is the same: loop over one thing, modify another. That means a snapshot, a new collection, or a list of things to remove later.
The error stops feeling random once you ask what the iterator is walking over. When you see dictionary changed size during iteration, go straight to the loop body and look for the del, pop, or assignment that changed the keys.
