Important:

for loop iterating an iterable is a lazy processing.

Understand this lazy processing

I am going to make two example to display how for...in loop work underneath the cover.
First one is loop over a enumerate object, and second one is loop over a range object. Both of them will modify the list inside the loop.

First example:

lst = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

for i, value in enumerate(lst):
   lst.pop(i)
print(lst)

There are No error raising for these codes, but they are not work as expected.

The code print below:

[1, 3, 5, 7, 9]

enumerate object is generator object which will yield value from lst. It is lazy process. When lst change, value from enumerate object change as well, but i is keep going.

for…in is equivalent to:

_iter = iter(enumerate_obj)
while 1:
 try:
   x = _iter.__next__()
 except StopIteration:
   break
 # statements

enumerate() is equivalent to:

def enumerate(lst):
    n = 0
    for i in lst:
        yield n, i
        n += 1

Second example:

lst = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

for i in range(len(lst)):
   lst.pop(i)

This code will raise IndexError

range() is iterable, but len(lst), the parameter of range(), have been replace with constant value 10. Because len(lst) is not a lazy process. It don’t change any more. So i will up to 9.

The codes are equivalent to:

for i in range(9):
   lst.pop(i)