Here's a bug that's particularly nasty because it sometimes works correctly:
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers:
if num % 2 == 0:
numbers.remove(num)
print(numbers)
# Expected: [1, 3, 5]
# Actual: [1, 3, 5, 6]
The goal was to remove all even numbers. But 6 survived.
Why This Happens
When you call remove() on a list, every item after the removed element shifts down by one index. The list gets shorter.
But the for loop doesn't know this happened. It maintains an internal counter that increments regardless. After processing index 1, it moves to index 2—even though what was at index 2 is now at index 1.
Let's trace through:
Initial: [1, 2, 3, 4, 5, 6]
Index 0: num=1, odd, keep it
Index 1: num=2, even, remove it → [1, 3, 4, 5, 6]
Index 2: num=4 (3 got skipped!), remove it → [1, 3, 5, 6]
Index 3: num=6 (5 got skipped!), now at end of (shorter) list
The bug is intermittent. Sometimes it appears to work. Sometimes it loses data. The worst kind of bug.
The Fixes
1. Iterate over a copy
for num in numbers[:]: # [:] creates a shallow copy
if num % 2 == 0:
numbers.remove(num)
The loop iterates over the copy (which doesn't change) while you modify the original.
2. Build a new list (better)
numbers = [num for num in numbers if num % 2 != 0]
This is the Pythonic approach. No mutation during iteration. No subtle bugs. List comprehensions are also faster than repeated remove() calls.
The Rule
Never modify a collection you're actively iterating over:
- Don't
remove()items from a list during aforloop over that list - Don't
delkeys from a dict while iterating over that dict - Don't
add()to a set while iterating over that set
Either iterate over a copy, or build a new collection with the items you want to keep.
Quick Reference
# DANGEROUS
for item in items:
items.remove(item) # Skips elements!
# SAFE: Copy
for item in items[:]:
items.remove(item)
# SAFE: New list (preferred for filtering)
items = [x for x in items if keep(x)]
From my upcoming book "Zero to AI Engineer: Python Foundations."
Top comments (0)