1

I would expect that in the following code the second loop overwrites the variable i of the first loop, so the inner loop will iterate i to have values 0, 1, 2 and then the outer loop, seeing that i=2 will end without doing the second iteration.

However the code seems to correctly do 6 iterations (depth 2)

I included printing i at each depth, to see in detail what is happening and it looks like the inner i is the one that can be accessed by name, while the object used by the outer loop seems to have lost a variable that references it, but still is usable by the loop.

for i in range(2):
   for i in range(3):
      print(f"i at depth 2: {i}")
   print(f"i at depth 1: {i}")
print(f"i at depth 0: {i}")

i at depth 2: 0
i at depth 2: 1
i at depth 2: 2
i at depth 1: 2
i at depth 2: 0
i at depth 2: 1
i at depth 2: 2
i at depth 1: 2
i at depth 0: 2

I'd be interested in knowing a bit better what is happening under the hood, it seems that loops don't really use variable names, but are directly tied to memory locations or something, so even if the outer loop object is no longer referenced by i it's still usable.

Is there any documentation on such behaviour that I can read to understand in detail what is happening?

Sembei Norimaki
  • 745
  • 1
  • 4
  • 11
  • see the steps [here](https://pythontutor.com/render.html#code=for%20i%20in%20range%282%29%3A%0A%20%20%20for%20i%20in%20range%283%29%3A%0A%20%20%20%20%20%20print%28f%22i%20at%20depth%202%3A%20%7Bi%7D%22%29%0A%20%20%20print%28f%22i%20at%20depth%201%3A%20%7Bi%7D%22%29%0Aprint%28f%22i%20at%20depth%200%3A%20%7Bi%7D%22%29&cumulative=false&curInstr=20&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) – eshirvana Jul 05 '23 at 14:31
  • Does this answer your question? [Why doesn't modifying the iteration variable affect subsequent iterations?](https://stackoverflow.com/questions/15363138/why-doesnt-modifying-the-iteration-variable-affect-subsequent-iterations) – Ignatius Reilly Jul 05 '23 at 15:19

2 Answers2

2

The line:

for i in range(2):

.. means "loop over the elements of range(2) (i.e., 0 and then 1) and assign the value to i during each iteration".

Similarly, the line:

    for i in range(3):

.. means "loop over the elements of range(3) (i.e, 0, 1, then 2) and assign the value to i during each iteration".

So, that's what happens:

  • 0 is assigned to i
    • 0 is assigned to i
      • 0 is printed
    • 1 is assigned to i
      • 1 is printed
    • 2 is assigned to i
      • 2 is printed
    • 2 is printed
  • 1 is assigned to i
    • 0 is assigned to i
      • 0 is printed
    • 1 is assigned to i
      • 1 is printed
    • 2 is assigned to i
      • 2 is printed
    • 2 is printed
  • 2 is printed

The fact that the value assigned to i in the outer loop is never used doesn't matter. Consider that this code would give the exact same result:

for _ in range(2):
   for i in range(3):
      print(f"i at depth 2: {i}")
   print(f"i at depth 1: {i}")
print(f"i at depth 0: {i}")

So, nothing complicated is happening - the loops are straightforward and the assignments happen in the order specified. It's not "i looping", it's just "the loop assigning to i as it progresses", and the name of the first loop variable is immaterial since it is never referenced before being overwritten.

Grismar
  • 27,561
  • 4
  • 31
  • 54
  • ok, still with C mentality, makes sense that since we are using an iterator, the iterator itself keeps track of its index, instead of the variable `i` being the one that keeps track of how long the loop has to iterate. – Sembei Norimaki Jul 05 '23 at 14:41
  • 1
    This is Python and not C, look at it this way: `range()` is a generator which keeps track of what value to yield next internally. When the iterator is exhausted, the loop will exit. So yes, the iterator keeps track of the value, and the value is assigned to `i` for use during the loop iteration. – Grismar Jul 05 '23 at 14:43
  • 1
    @SembeiNorimaki here there's an [analogy with C code](https://stackoverflow.com/a/15363559/15032126) – Ignatius Reilly Jul 05 '23 at 15:22
2

This is a broadly equivalent implementation without for loops, to illustrate what happens to i. Note that using i for both loops doesn't cause an issue, since it's the iterators it1 and it2 that book-keep the iterations.

it1 = iter(range(2))
while True:
    try:
        i = next(it1)
        it2 = iter(range(3))
        while True:
            try:
                i = next(it2)
                print(f"i at depth 2: {i}")
            except StopIteration:
                break
        print(f"i at depth 1: {i}")
    except StopIteration:
        break
print(f"i at depth 0: {i}")

prints (like your original):

i at depth 2: 0
i at depth 2: 1
i at depth 2: 2
i at depth 1: 2
i at depth 2: 0
i at depth 2: 1
i at depth 2: 2
i at depth 1: 2
i at depth 0: 2

Comparing across languages, Python's for is more like the "for each" construction in various languages than like C for. In fact, C for is closer to Python while. For example:

for (i = 1; i <= 1024; i *= 2) {
    foo();
}

is best translated to Python as:

i = 1
while i <= 1024:
    foo()
    i *= 2
slothrop
  • 3,218
  • 1
  • 18
  • 11