for y in range(y) in Python

Viewed 497

This may seem like a silly question, but I've actually stumped myself. I was trying to write a "pyramid" pattern code, and I succeeded, but I have no idea how I did it. The first half of the pyramid was obvious, the second half I did by mistake, and I can't figure out why the code actually works.

Here is my code:

for x in range(5): # I understand why this loop works, its fairly simple
    for y in range(x+1):
        print(y, end='')
    print('')

for x in range(5):
    for y in range(y): # This is the part I can't seem to wrap my head around
        print(y, end='')
    print('')

The output is:

0
01
012
0123
01234
0123
012
01
0

I've been staring at it and I can't understand how for y in range(y) actually works.

4 Answers

The range object describes a half-open interval. This means its upper bound is not included in it:

>>> 5 in range(5)
False

A for loop evaluates its iterable once. Re-using the iterable name or its arguments does not affect the loop.

>>> a = 3
>>> for b in range(a):
...     print(b, a)
...     a += 1  # does not extend the range!
0 3
1 4
2 5

That also means it is safe for the iteration variable to be used in the iterable. By the time the loop assigns to the iteration variable, the iterable has already been evaluated.


That means a construct such as for y in range(y): will a) safely assign to y without changing the current iteration, and b) iterate y from 0 to y-1. In other words, the loop works regularly but leaves y reduced by one afterwards.

This effect happens in the second loop: y is steadily reduced by 1 each time the for y in range(y) loop runs. The outer loop merely runs it often enough to reach 0. Compare:

>>> y = 4
>>> while y:
...     print(y, ':', end='')
...     for y in range(y):
...         print(y, end='')
...     print('\t=>', y)
4 :0123 => 3
3 :012  => 2
2 :01   => 1
1 :0    => 0

There is one variable y in your code. In the first block it's set to 1, 2, 3 and 4. The variable keeps the last value in the second block.

You can see it in this code

for x in range(5):
    for y in range(x+1):
        print(y, end='')
    print('')

print('Last y after first block: {}'.format(y)) # y has the value 4

for x in range(5):
    for y in range(y):
        print(y, end='') # y decreases in each iteration because range(y) goes from 0 through y-1 and y-1 is the last value
    print('')
    print('Last y after inner loop: {}'.format(y)) # y has the value y-1

y is created in in the first block and exists until the end of this code.

Python has weird scoping rules. On its own, the second loop will not work:

>>> for y in range(y): print(y)
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'y' is not defined

However, since you also have the first loop, and since for loops do not make their own scopes, y survives from the first loop with the last value it is given: 6.

Then the second loop is for y in range(6) but then the y keeps counting down since range is exclusive on the right side.

This is unnecessarily tricky. Variable y happens to start at 4 because of the preceding loop (which is an unreliable assumption at best and implementation dependent at worst). Combined with the reassigmnent of the existing y variable toy-1 in the second loop, it becomes very difficult to follow.

A much cleaner approach would be to use the same logic as the previous loop but go through the values of x in reverse:

for x in range(5):       # forward loop
    for y in range(x+1):
        print(y, end='')
    print('')

for x in reversed(range(4)): # backward loop mirrors the previous one
    for y in range(x+1):
        print(y, end='')
    print('')

or in a single loop if you like using a bit of math:

N = 5
for x in range(1-N,N):
    for y in range(N-abs(x)):
        print(y,end='')
    print()
Related