Python Language Common Pitfalls Changing the sequence you are iterating over

Help us to keep this website almost Ad Free! It takes only 10 seconds of your time:
> Step 1: Go view our video on YouTube: EF Core Bulk Insert
> Step 2: And Like the video. BONUS: You can also share it!

Example

A for loop iterates over a sequence, so altering this sequence inside the loop could lead to unexpected results (especially when adding or removing elements):

alist = [0, 1, 2]
for index, value in enumerate(alist):
    alist.pop(index)
print(alist)
# Out: [1]

Note: list.pop() is being used to remove elements from the list.

The second element was not deleted because the iteration goes through the indices in order. The above loop iterates twice, with the following results:

# Iteration #1
index = 0
alist = [0, 1, 2]
alist.pop(0) # removes '0'

# Iteration #2
index = 1
alist = [1, 2]
alist.pop(1) # removes '2'

# loop terminates, but alist is not empty:
alist = [1]

This problem arises because the indices are changing while iterating in the direction of increasing index. To avoid this problem, you can iterate through the loop backwards:

alist = [1,2,3,4,5,6,7]
for index, item in reversed(list(enumerate(alist))):
    # delete all even items
    if item % 2 == 0:
        alist.pop(index)
print(alist)
# Out: [1, 3, 5, 7]

By iterating through the loop starting at the end, as items are removed (or added), it does not affect the indices of items earlier in the list. So this example will properly remove all items that are even from alist.


A similar problem arises when inserting or appending elements to a list that you are iterating over, which can result in an infinite loop:

alist = [0, 1, 2]
for index, value in enumerate(alist):
    # break to avoid infinite loop:
    if index == 20:     
        break           
    alist.insert(index, 'a')
print(alist)
# Out (abbreviated): ['a', 'a', ..., 'a', 'a',  0,   1,   2]

Without the break condition the loop would insert 'a' as long as the computer does not run out of memory and the program is allowed to continue. In a situation like this, it is usually preferred to create a new list, and add items to the new list as you loop through the original list.


When using a for loop, you cannot modify the list elements with the placeholder variable:

alist = [1,2,3,4]
for item in alist:
    if item % 2 == 0:
        item = 'even'
print(alist)
# Out: [1,2,3,4]

In the above example, changing item doesn't actually change anything in the original list. You need to use the list index (alist[2]), and enumerate() works well for this:

alist = [1,2,3,4]
for index, item in enumerate(alist):
    if item % 2 == 0:
        alist[index] = 'even'
print(alist)
# Out: [1, 'even', 3, 'even']

A while loop might be a better choice in some cases:

If you are going to delete all the items in the list:

zlist = [0, 1, 2]
while zlist:
    print(zlist[0])
    zlist.pop(0)
print('After: zlist =', zlist)

# Out: 0
#      1
#      2
# After: zlist = []

Although simply resetting zlist will accomplish the same result;

zlist = []

The above example can also be combined with len() to stop after a certain point, or to delete all but x items in the list:

zlist = [0, 1, 2]
x = 1
while len(zlist) > x:
    print(zlist[0])
    zlist.pop(0)
print('After: zlist =', zlist)

# Out: 0
#      1
# After: zlist = [2]

Or to loop through a list while deleting elements that meet a certain condition (in this case deleting all even elements):

zlist = [1,2,3,4,5]
i = 0
while i < len(zlist):
    if zlist[i] % 2 == 0:
        zlist.pop(i)
    else:
        i += 1
print(zlist)
# Out: [1, 3, 5]

Notice that you don't increment i after deleting an element. By deleting the element at zlist[i], the index of the next item has decreased by one, so by checking zlist[i] with the same value for i on the next iteration, you will be correctly checking the next item in the list.


A contrary way to think about removing unwanted items from a list, is to add wanted items to a new list. The following example is an alternative to the latter while loop example:

zlist = [1,2,3,4,5]

z_temp = []
for item in zlist:
    if item % 2 != 0:
        z_temp.append(item)
zlist = z_temp
print(zlist)
# Out: [1, 3, 5]

Here we are funneling desired results into a new list. We can then optionally reassign the temporary list to the original variable.

With this trend of thinking, you can invoke one of Python's most elegant and powerful features, list comprehensions, which eliminates temporary lists and diverges from the previously discussed in-place list/index mutation ideology.

zlist = [1,2,3,4,5]
[item for item in zlist if item % 2 != 0]
# Out: [1, 3, 5]


Got any Python Language Question?