Why does calling list.remove inside a for loop over that same list skip elements?
answer
- The loop keeps a position, not a snapshot
- Deletion shifts the tail down one
- Index up, elements down
- Neighbour slides into the visited slot
- Copy, rebuild, or walk backwards
basics
~20 sA list iterator holds an integer index and advances it on every pass. Deleting an element shifts everything after it down one slot, so the next index lands one element too far and the shifted item is never visited.
solid answer
~40 s`for x in lst` creates a list iterator that stores nothing but a reference to the list and a position counter starting at 0; each pass hands back `lst[i]` and increments `i`. The iterator takes no snapshot, so `lst.remove(v)` or `del lst[i]` inside the body physically shifts every later element down one index while the counter still moves up one. The element that slides into the slot you just visited is stepped straight over — with `[1, 2, 2, 3, 2, 4]` and a `remove` on every `2`, one `2` survives. Nothing raises; the loop just quietly does the wrong thing. The fixes are to iterate over a copy (`for x in lst[:]`), rebuild the list with a comprehension, or walk indices in reverse so the shift only touches elements already seen.
code
python · 5 linesnums = [1, 2, 2, 3, 2, 4]
for n in nums:
if n == 2:
nums.remove(n)
print(nums) # [1, 3, 2, 4] -- one 2 survived, no exceptiongo deeper
Recall the shape of the bug and one fix you can write from memory: iterate lst[:] or rebuild with a comprehension. Be able to say that no exception is raised, so the damage is silent.
Explain the mechanics out loud: the iterator holds a live reference plus an index, deletion shifts the tail down, the index still moves up. Trace a short list on the whiteboard rather than asserting the rule.
Show how the bug reaches production — a filter that removes 'about half' of what it should, with no log line and no exception — and how you would catch it: a test with two adjacent matches, and a review habit of flagging mutation inside a loop over the same object.
Own the guidance: prefer building new collections over in-place edits in shared code, and state when in-place slice assignment is required because other holders of the reference must see the change. Decide whether the team's lint configuration should flag mutation of the loop's own subject.
## What a `for` loop over a list actually does `for x in lst:` is sugar for three steps: call `iter(lst)` once, call `__next__` on the resulting iterator on every pass, and stop when that call raises `StopIteration`. The object `iter` returns for a list is a plain iterator object, and its entire state is two things: a reference to the *same* list object you are looping over, and an integer index that starts at 0. Each `__next__` compares the index against the list's *current* length, returns the element at that index, and increments the index. Two consequences follow, and both surprise people: * The iterator is **not** a snapshot. It never copies the elements. It re-reads the live list every pass, so any change you make to the list is visible on the very next pass. * The stop condition is re-evaluated against the live length, not against the length recorded when the loop started. ## Why deletion skips A Python list is a contiguous array of pointers. Removing an element that is not the last one is implemented by moving every following pointer down one slot and shrinking the length. `list.remove(v)` finds the first element equal to `v` and does exactly that; `del lst[i]` does it by index; `list.pop(i)` does it and hands the element back. So on the pass where you delete, two counters move in opposite directions. Everything after the deleted position moves *down* by one, and the iterator's index moves *up* by one. The element that just slid into the position you were standing on is now behind the cursor and will never be visited. Trace `[1, 2, 2, 3, 2, 4]` with `if n == 2: nums.remove(n)`: ``` i=0 -> 1 list: [1, 2, 2, 3, 2, 4] i=1 -> 2 del list: [1, 2, 3, 2, 4] (the second 2 slid into index 1) i=2 -> 3 list: [1, 2, 3, 2, 4] -- index 1 was skipped i=3 -> 2 del list: [1, 2, 3, 4] i=4 >= len(4) loop ends ``` The result is `[1, 3, 2, 4]`: one `2` survives, and it survives *silently*. Every second element of a run is skipped, which is why the bug is so often reported as "it only removes half of them" or "it works until there are two in a row". ## The mirror image: growth that never terminates The same index arithmetic produces the opposite failure when you *append* inside the loop. The iterator's index climbs by one per pass while the length climbs by one too, so the stop condition is never reached and the loop runs until memory is exhausted. This is the same mechanism, not a second one: the iterator asks the live list how long it is, every single pass. ## Why this is silent, unlike a dict or a set Hash-based containers can rehash and move entries anywhere, so their iterators refuse to continue after a size change and raise `RuntimeError`. A list's iterator has no such guard, because index-based iteration over a shifting array is well defined — it just does not mean what you wanted. Python's choice here is deliberate: the list iterator is a two-word object and checking for concurrent modification would cost every well-behaved loop. ## The four ways out 1. **Iterate a copy.** `for n in lst[:]` (or `list(lst)`) builds a new list first, so the thing being walked and the thing being mutated are different objects. Simple and the closest to the code you already wrote. It costs one shallow copy, and each `remove` is still a linear scan. 2. **Rebuild with a comprehension.** `lst = [n for n in lst if n != 2]` reads as a filter, is a single linear pass, and has no mutation problem at all because it never touches the original. This is the idiomatic answer. 3. **Rebuild in place.** `lst[:] = [n for n in lst if n != 2]` when other names, attributes or structures already hold a reference to the same list object and must see the change. Plain assignment only rebinds your local name; slice assignment mutates the object. 4. **Walk backwards by index.** `for i in range(len(lst) - 1, -1, -1): ... del lst[i]`. Deleting at index `i` only shifts elements *after* `i`, and those have already been visited, so nothing is skipped. Use this when the work is genuinely index-based; note that the safe reverse form deletes by index, not by value. ## Things that do not help `enumerate(lst)` does not help — it wraps the same underlying iterator and its counter advances the same way, so the index it reports simply becomes wrong after the first deletion. Neither does converting the loop to a `while` with a manual index unless you skip the increment on the passes where you deleted, which is the fiddly hand-rolled version of walking backwards. And catching an exception does not help either, because there is no exception to catch. ## What interviewers listen for The give-away of a shallow answer is "you cannot modify a list while iterating it". You can — Python permits it and defines what happens. The precise answer names the index-based iterator, explains that deletion shifts the tail down while the index moves up, and offers the copy/rebuild/reverse fixes with a reason for choosing among them.
- What happens instead if you append to the list inside the loop?The loop may never end. The iterator compares its index against the list's current length on every pass, so growing the list by one per pass keeps the stop condition permanently out of reach and the loop runs until memory is exhausted. It is the same mechanism as the skipping bug, pointed the other way: the iterator reads the live list rather than a snapshot.
- Does enumerate protect you from the skipping problem?No. `enumerate` wraps the same list iterator and adds a counter of its own, so the underlying index still advances one per pass and elements are still skipped after a deletion. Worse, the number `enumerate` yields stops matching the element's real position as soon as you delete anything, so using it to index back into the list gives you the wrong element.
- Why is `lst[:] = [...]` sometimes required instead of `lst = [...]`?Plain assignment rebinds only your local name; the original list object is untouched, so any other name, attribute, or container still referring to it sees the unfiltered contents. Slice assignment replaces the contents of the existing object in place, so every holder of that reference sees the filtered list. Use it when the list is shared, and prefer the cheaper plain rebind when it is not.
- Is walking the list in reverse always safe when deleting?It is safe when you delete by index at the current position, because that only shifts elements after it, and those have already been visited. It is not automatically safe if you delete by value: `list.remove` removes the first equal element, which may sit ahead of the cursor and shift the elements you have not reached yet. Reverse loops should use `del lst[i]`.
It is like a ticket inspector walking a train row by row, counting rows as they go, while rows are being unbolted behind them: pull out row 2 and row 3 becomes the new row 2, but the inspector has already moved on to row 3.
saying these in an interview costs you the question
- Claims Python forbids modifying a list while iterating it
- Expects a RuntimeError from list deletion during iteration
- Thinks the loop iterates a snapshot copy of the list
- Believes enumerate makes in-loop deletion safe
- Says lst = [...] updates the list other code already holds
- Blames list.remove for removing the wrong element