Control flow · about 40 min

for loops and range

Why this matters

Most repetition in real programs is "do this for each thing": each character in a string, each line in a file, each number from 1 to 100, each user in a list. With while you must manage a counter yourself, and forgetting to update it gives an infinite loop. Python's for loop handles all of that: it walks through a collection, one item at a time, and stops by itself at the end. It is the loop you will write most often.

for over a string

for ch in "cat":
    print(ch)
c
a
t

Read it as "for each ch in "cat", run the block". On every iteration Python binds the loop variable ch to the next item, here the next one-character string, then runs the body. When there are no more items, the loop ends.

Compare the equivalent while loop:

text = "cat"
i = 0
while i < len(text):
    ch = text[i]
    print(ch)
    i += 1

The for version has no index, no len, no update step and no way to get stuck. When you need each item of something, prefer for.

What for really does

for works with any iterable: an object that can produce its items one at a time. Strings, ranges, lists, dictionaries, files and many more are iterable. Underneath, for asks the object for an iterator (by calling iter() on it) and then repeatedly asks the iterator for the next item (with next()), until the iterator signals that it is exhausted. You will build your own iterators in a later module; for now, "for gives you each item in turn" is the right model.

range: sequences of numbers

To repeat something a number of times, or to loop over numbers, use range:

for i in range(5):
    print(i, end=" ")
print()
for i in range(2, 6):
    print(i, end=" ")
print()
for i in range(0, 20, 5):
    print(i, end=" ")
print()
for i in range(5, 0, -1):
    print(i, end=" ")
print()
0 1 2 3 4
2 3 4 5
0 5 10 15
5 4 3 2 1

range follows the same half-open rule as slicing:

CallProducesRule
range(stop)0, 1, ..., stop-1starts at 0
range(start, stop)start, ..., stop-1stop excluded
range(start, stop, step)start, start+step, ...stops before passing stop

So range(n) produces exactly n numbers, and range(1, n + 1) gives 1 to n inclusive. A step can be negative to count down; a range that would go the wrong way is simply empty (range(5, 0) produces nothing, so the loop body never runs).

range is lazy

A range object does not store all its numbers. It stores only start, stop and step, and computes each value when asked. range(10**12) takes a few dozen bytes of memory, not terabytes:

r = range(0, 10**12, 3)
print(r)
print(len(r))
print(r[1000])
print(999 in r)
range(0, 1000000000000, 3)
333333333334
3000
True

len, indexing and in are all computed with arithmetic, no looping needed. This is a first taste of lazy evaluation, a theme that returns with generators.

The loop variable

The loop variable is an ordinary name, rebound on each iteration. Two consequences:

for i in range(3):
    print(i)
    i = 100      # no effect on the next iteration
0
1
2

If you do not need the variable at all, the convention is to call it _:

for _ in range(3):
    print("hip hip hooray")

Accumulating with for

The accumulator pattern from the last lesson works the same way, and more safely:

n = int(input())
total = 0
product = 1
for k in range(1, n + 1):
    total += k
    product *= k
print(f"sum of 1..{n} = {total}")
print(f"{n}! = {product}")

With input 10:

sum of 1..10 = 55
10! = 3628800

Counting is accumulating with += 1 when a condition holds. Combining for, if and an accumulator handles a huge range of problems:

text = input()
digits = 0
spaces = 0
for ch in text:
    if ch.isdigit():
        digits += 1
    elif ch == " ":
        spaces += 1
print(f"digits={digits} spaces={spaces}")

With input Room 101 at 9am:

digits=4 spaces=3

Building strings

Strings are immutable, so "building a string" means creating a new one each time and rebinding the name:

word = input()
spaced = ""
for ch in word:
    spaced += ch + " "
print(spaced.strip())

With input python, this prints p y t h o n. For a handful of pieces this is fine. For thousands, collect pieces in a list and join them at the end (lists come in the collections module), because each += may copy everything built so far.

Nested loops

A loop body can contain another loop. The inner loop runs completely for each iteration of the outer one:

for row in range(1, 4):
    line = ""
    for col in range(1, 5):
        line += f"{row * col:4}"
    print(line)
   1   2   3   4
   2   4   6   8
   3   6   9  12

The outer loop ran 3 times; the inner loop ran 4 times per outer iteration, so the inner body ran 3 x 4 = 12 times. This multiplication matters: two nested loops over n items each do n * n steps, and that grows quickly. A nested loop over a million items would run a trillion iterations.

for or while?

Common mistakes

Recap

Going further: try print(list(range(5, 50, 7))) in the REPL to see a range's values all at once, and import sys; print(sys.getsizeof(range(10**9))) to see how little memory a huge range uses.

Exercises

Exercise 1 · quiz

What does range produce?

What does this print? Type the numbers separated by single spaces.

for i in range(10, 0, -3):
    print(i, end=" ")
Answer

range(start, stop, step) starts at 10 and adds -3 each time while the value is still beyond the stop in the step's direction: 10, 7, 4, 1. The next value, -2, would pass the stop 0, so it is excluded. As with slices, the stop value itself is never produced.

Exercise 2 · write code

Times table

Read a whole number n and print its multiplication table from 1 to 10, one line each, in exactly this format (shown for n = 7, first three and last lines):

7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
...
7 x 10 = 70

Use a for loop with range.

File: main.py

Starting point

n = int(input())
for i in range(10):
    print(f"{n} x {i} = {n * i}")

Example

input

7

expected output

7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
7 x 4 = 28
7 x 5 = 35
7 x 6 = 42
7 x 7 = 49
7 x 8 = 56
7 x 9 = 63
7 x 10 = 70

Example

input

-2

expected output

-2 x 1 = -2
-2 x 2 = -4
-2 x 3 = -6
-2 x 4 = -8
-2 x 5 = -10
-2 x 6 = -12
-2 x 7 = -14
-2 x 8 = -16
-2 x 9 = -18
-2 x 10 = -20
Hint 1

range(10) gives 0 to 9. You need 1 to 10.

Hint 2

range(1, 11) starts at 1 and stops before 11.

Reference solution
n = int(input())
for i in range(1, 11):
    print(f"{n} x {i} = {n * i}")

Exercise 3 · write code

Count vowels

Read one line of text and print the number of vowels it contains (a e i o u, upper or lower case), in this format:

vowels: 3

(That is for the input Hello World, whose vowels are e, o and o.) Loop over the characters with for.

File: main.py

Starting point

text = input()
count = 0
print(f"vowels: {count}")

Example

input

Hello World

expected output

vowels: 3

Example

input

AEIOU and sometimes Y

expected output

vowels: 10
Hint 1

for ch in text: gives one character per iteration.

Hint 2

ch in "aeiou" tests whether a character is a vowel; lowercase the text first to handle capitals.

Reference solution
text = input()
count = 0
for ch in text.lower():
    if ch in "aeiou":
        count += 1
print(f"vowels: {count}")

Exercise 4 · write code

A right-aligned staircase

Read a whole number n (1 or more) and print a staircase of # characters, n lines high, right-aligned so that the last line has no leading spaces. For n = 4:

   #
  ##
 ###
####

Line i (counting from 1) has n - i spaces followed by i hash signs. Recall that " " * 3 is " ".

File: main.py

Starting point

n = int(input())
for i in range(n):
    print("#" * i)

Example

input

4

expected output

   #
  ##
 ###
####

Example

input

1

expected output

#
Hint 1

Use range(1, n + 1) so that i runs from 1 to n.

Hint 2

Build each line from two repeated strings: " " * (n - i) + "#" * i.

Reference solution
n = int(input())
for i in range(1, n + 1):
    print(" " * (n - i) + "#" * i)

Exercise 5 · write code

Primes up to n

Read a whole number n and print all prime numbers from 2 up to and including n, on one line, separated by single spaces. A prime is a whole number greater than 1 whose only divisors are 1 and itself. For n = 20:

2 3 5 7 11 13 17 19

If there are no primes (n less than 2), print none.

Use nested for loops: for each candidate, count (or detect) divisors between 2 and the candidate minus 1. Build the output line in a string as you go. For full marks on speed, it is enough to test divisors d while d * d <= candidate, but the simple version is fast enough for the checks here.

File: main.py

Starting point

n = int(input())
line = ""
print(line)

Example

input

20

expected output

2 3 5 7 11 13 17 19

Example

input

2

expected output

2
Hint 1

Outer loop: for candidate in range(2, n + 1):. Inner loop: try each divisor d from 2 upward.

Hint 2

Use a flag variable: set is_prime = True before the inner loop and False when you find a divisor.

Hint 3

Append each prime to a string with a trailing space, then print line.strip() at the end.

Reference solution
n = int(input())
line = ""
for candidate in range(2, n + 1):
    is_prime = True
    for d in range(2, candidate):
        if d * d > candidate:
            break
        if candidate % d == 0:
            is_prime = False
            break
    if is_prime:
        line += str(candidate) + " "
if line:
    print(line.strip())
else:
    print("none")