Python: remove items from a list while iterating
Does your Python loop leave items behind after remove? Trace the indices, filter without changing the input, and decide when to update the list in place.
You remove even numbers in a for loop, yet a 4 stays in the list. No error message. The result depends on where the removed values sit.
In short
To remove items from a Python list by a condition, build a new list with a comprehension. Calling remove while iterating over that same list shifts later items, so the loop can skip them. If other references must see the change, assign the result to values[:] rather than to the name values.
Why does remove skip items?
A list iterator advances to the next index while a deletion shifts the items. This example puts two even numbers next to each other. enumerate prints the iteration count, which here matches the index requested by the iterator. The list is printed before each deletion.
values = [2, 4, 5, 6]for index, x in enumerate(values): print(index, x, values) if x % 2 == 0: values.remove(x)print(values)$ python3 skip.py0 2 [2, 4, 5, 6]1 5 [4, 5, 6]2 6 [4, 5, 6][4, 5]The first iteration removes 2. The 4 moves from index 1 to index 0, but iteration continues at index 1, where it finds 5. The 4 never gets tested.
| Iteration | List before the test | Value read | After any deletion |
|---|---|---|---|
| 0 | [2, 4, 5, 6] | 2 | [4, 5, 6] |
| 1 | [4, 5, 6] | 5 | [4, 5, 6] |
| 2 | [4, 5, 6] | 6 | [4, 5] |
The issue isn't a copy made by for. The iterator reads the changing list. An example where odd numbers always separate even numbers may appear to work; it doesn't test consecutive deletions.
How do you filter without changing the input list?
A comprehension creates a new list containing the values to keep. To remove even numbers, keep values whose remainder after division by 2 is nonzero.
values = [2, 4, 5, 6]result = [x for x in values if x % 2 != 0]print(result)print(values)print(result is values)$ python3 filter.py[5][2, 4, 5, 6]FalseThe result contains 5, and the input still contains all four values. The is test prints False because the names refer to separate lists. Retained duplicates keep their original order. An empty input produces a new empty list.
That contract matters when you write a function called by other code: its caller may need to reuse the input. The traversal in binary search also leaves its array intact; here, we build a separate result.
merge_sorted_values
Merge two already sorted integer lists without changing either input. This challenge isn't a filtering task: it practices the same choice of building a result while traversing data that must stay intact. Preserve duplicates too.
What is the difference between values = ... and values[:] = ...?
Assigning to a name replaces its reference. Assigning to a slice replaces the contents of the existing list, which other references still see.
values = [2, 4, 5, 6]alias = valuesvalues = [x for x in values if x % 2 != 0]print(values, alias, values is alias)
values = [2, 4, 5, 6]alias = valuesvalues[:] = [x for x in values if x % 2 != 0]print(values, alias, values is alias)$ python3 aliases.py[5] [2, 4, 5, 6] False[5] [5] TrueIn the first half, alias keeps the original list. In the second half, both names still refer to the same object, and both see its filtered contents. Choose based on the expected contract, not on which spelling is shorter.
The comprehension on the right of the slice assignment finishes before the modification. It therefore avoids deleting items during its own traversal. It still allocates a temporary list: updating the original list doesn't mean using no extra memory.
Can you iterate over a copy and call remove?
Yes. If your processing needs a loop with several actions, iterating over a copy separates the visited items from the list being modified.
values = [2, 2, 4, 5, 6]for x in values.copy(): if x % 2 == 0: values.remove(x)print(values)$ python3 snapshot.py[5]The copy supplies both occurrences of 2, even after the first has been removed from values. remove(x) deletes the first item equal to x in the target list. This code assumes that only this deletion changes the list during processing; another action that already removes x could make remove raise ValueError.
For a plain filter, prefer the comprehension. Copying and then repeatedly searching and shifting with remove does more work. A list copy is shallow: if its items are themselves lists, their contents don't become independent.
Frequently asked questions
Why doesn't enumerate fix this?
enumerate adds a counter to iteration. It doesn't freeze list positions or prevent the shifts caused by a deletion.
What about empty lists and duplicates?
The comprehension accepts an empty list. It tests every occurrence and keeps duplicates that meet the condition in their original order.
Can I use del instead of remove?
del values[i] removes by index, but it also shifts later items. During forward iteration, replacing remove with del doesn't fix that shift.
Is assigning values = values.copy() before the loop enough?
No, if you then iterate over values and modify values: you're traversing and modifying the same new list. The loop must traverse a separate copy, as in snapshot.py.
Sources
