What happens if you modify a collection while looping over it?

A dict raises RuntimeError: dictionary changed size during iteration. A list does something worse: it silently skips elements, because removing one shifts everything left while the index keeps advancing. Iterate over a copy, or build a new collection.

Overview

What goes wrong

Removing items from a list while looping over it silently skips elements. It does not raise, which is what makes it dangerous.

Python

The mechanism: list iteration is driven by an internal index. Removing element 1 shifts everything down one slot, and the index still advances — so the element that moved into the vacated position is never examined.

StepIndexListSeesAction
10[1, 2, 2, 3]1keep
21[1, 2, 2, 3]2remove → [1, 2, 3]
32[1, 2, 3]3keep — the second 2 was skipped

The list is now [1, 2, 3] and the loop is finished.

Dictionaries and sets behave differently — they raise:

Python

Which is far better, because the failure is loud. Dicts and sets track a version counter and detect the change. Lists do not, so they fail quietly instead.

CollectionMutating during iteration
listSilently skips elements
dictRuntimeError on size change
setRuntimeError on size change
dequeRuntimeError
GeneratorDepends on the source

Note the dict wording: size change. Reassigning an existing key's value is allowed and safe; adding or deleting keys is not.

Dicts, sets & hashingConceptualEasy

Step through it

dict
a:10
b:21
c:32
state
iterating : position 0
size : 3
Iteration starts. The dict records its size, and every step checks that it has not moved.
size3step0
Step 1 of 4

What to watch

  • The dict records its size and checks it on every step.
  • The error names the real cause — a size change.
  • Rebuilding rather than mutating avoids the question entirely.

Say this out loud

"Dicts raise RuntimeError. Lists don't - they silently skip elements, because deleting shifts the rest left while the index moves right. I iterate over list(d) or a slice copy, or better, build a new collection with a comprehension."

What happens if you modify a collection while looping over it?

Why can't you delete from a dict or a list while iterating it, and what should you do instead?

Run it

The dict raising, the list silently skipping on the same logical operation, and the three fixes all producing the answer the broken loop was supposed to give.

The four correct approaches

Build a new collection — clearest, and the default choice:

Iterate over a copy when the original object must be mutated in place, because other references point at it:

list(d) is the idiom for dictionaries — it materialises the keys before the loop starts, so deletions during the loop cannot disturb it.

Slice-assign to mutate in place with no copy semantics visible to callers:

This is the underused one. It keeps the same list object — so aliases see the change — while doing the filtering safely.

Iterate backwards when removing by index, since removals then only affect positions already visited:

Two different failures

A dictionary keeps a version counter and compares it on each step, so a size change during iteration is caught immediately and loudly. That is a feature: the alternative is undefined behaviour, because a resize can move every entry to a different slot.

A list has no such check. Deleting element i shifts everything after it one place left, while the loop's internal index advances — so the element that moved into position i is never visited. You get a wrong answer and no error at all, which is the harder bug to find.

The fixes, in order of preference

Build a new collection. [x for x in items if keep(x)] or {k: v for k, v in d.items() if keep(k)}. Nothing is mutated, the intent is explicit, and it is usually faster than repeated deletion.

Iterate over a snapshot. for k in list(d): or for x in items[:]:. Necessary when you genuinely must mutate in place — because other references to the object are watching.

Filter in place backwards. Walking from the end means deletions only shift elements you have already passed. Correct, and worth knowing for when memory matters.

What counts as a change

For a dict, only a size change trips the check. Assigning to an existing key is fine, which surprises people who expect any mutation to raise. Adding a key raises just as deletion does.

Sets behave like dicts. collections.deque also raises. And modifying the object a variable points to — appending to a list stored as a dict value — is not a change to the dict, so it is perfectly legal.

Which to choose

SituationApproach
Filtering, no aliasing concernsComprehension — nums = [...]
Must keep the same objectSlice assignment — nums[:] = [...]
Removing by indexIterate backwards
Dictionary keysfor k in list(d)
Very large collection, memory-boundIn-place two-pointer compaction
Adding while iteratingCollect additions, extend after the loop

The aliasing distinction is the one that causes real bugs:

If a function is documented as modifying its argument, the comprehension form silently does nothing from the caller's perspective. Slice assignment is what actually mutates.

For the memory-bound case, two-pointer compaction filters in place with no second list:

O(n) time, O(1) extra space — the same pattern as "remove duplicates in place".

Adding during iteration

Growing a list while iterating it does not raise either — and it can loop forever:

The index keeps finding new elements. The fix is to accumulate separately:

The same discipline applies to a worklist algorithm, where the natural structure is an explicit queue rather than a for loop:

That is the correct shape for BFS, dependency resolution and crawling — anything where processing an item produces more items.

Questions people ask

Why does a list not raise like a dict? Lists have no version counter; iteration is a plain index. Dicts and sets track modifications because rehashing could otherwise corrupt the traversal.

Is nums[:] a deep copy? No, shallow — the new list holds the same element references. Fine for filtering, not for mutating the elements themselves.

Can I change dict values while iterating? Yes. Only size changes raise.

What about for k, v in d.items() and then del? Same error. Wrap in list(d.items()).

Is iterating backwards a hack? No — it is correct and O(n), and it is the right choice when removing by index in place.

What is fastest? A comprehension, usually — one pass, one allocation, and the loop runs in C.

Does this apply to generators? A generator over a mutating source gives undefined results; materialise it first if the source will change.

Recap in one screen

  • Removing from a list while iterating silently skips elements, because the index advances past the shifted-down item.
  • Dicts and sets raise RuntimeError on a size change — a loud failure, which is better.
  • Prefer a comprehension; use nums[:] = [...] when the caller's object must be mutated.
  • For dictionaries, iterate list(d) to snapshot the keys first.
  • Growing a list while iterating it can loop forever — collect additions and extend afterwards, or use an explicit worklist.

Check yourself

0 of …

Answer without scrolling back up.

  1. Deleting from a list while iterating over it:

  2. Which dict operation during iteration is legal?

  3. The preferred fix is:

Cheat sheet

What happens if you modify a collection while looping over it?

A dict raises RuntimeError: dictionary changed size during iteration. A list does something worse: it silently skips elements, because removing one shifts everything left while the index keeps advancing. Iterate over a copy, or build a new collection.

INTERVIEW · AI.Cruz.in/interview/modifying-a-collection-while-iterating.html

About the author

AI.Cruz builds and maintains AI.Cruz. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.