Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~31 min· topic 4 of 9

Topic 2.4

for Loops and range()

In one line

A for loop visits each item of a sequence one by one, and range(start, stop, step) produces the numbers for counting loops. range stops before stop and is lazy: it never builds the whole list in memory.

Think of it like this

A teacher calling the roll. She takes the register and reads the names one at a time, from the first to the last, and marks each child. She never needs to count how many children there are, and she cannot forget to move to the next name. A for loop is that: take each item in turn and do the same work for it.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Collection
A group of values kept together, such as a list of names.
Iterable
Anything a for loop can go through item by item: a list, a string, a range and more.
Loop variable
The name that holds the current item on each pass, such as fruit in for fruit in fruits.
range()
A built-in that represents a sequence of whole numbers: range(start, stop, step).
Step
The difference between one number and the next in a range. A negative step counts down.
Lazy
Producing values only when they are needed, instead of building them all first.
Off-by-one error
A bug where a loop runs one time too many or one too few, usually from forgetting that stop is excluded.
Underscore `_`
By convention, a name for a value you do not intend to use.
Empty range
A range with no numbers, such as range(5, 1). A loop over it runs zero times.

Step by step

01Visiting each item

A list in square brackets holds several values in order (Phase 4 covers lists fully). The loop takes the first fruit, sets fruit to it, runs the block, then takes the next, and so on. Three items, three passes.

main.pywhole filepython
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print("Fruit:", fruit)
Visiting each itemdiagram
Rendering diagram…

02Looping over a string

A string is a sequence of characters (Topic 1.5), so for visits them one by one. Spaces and punctuation count as characters.

main.pywhole filepython
for letter in "Hi!":
    print(letter)

03range with one number

range(5) means five numbers starting at 0: 0, 1, 2, 3, 4. The last one is 4, not 5. The list(...) wrapper turns the lazy range into a list so you can see the numbers.

main.pywhole filepython
print(list(range(5)))
for i in range(3):
    print("pass number", i)

04Start, stop and step

Two numbers set the start and the stop. A third sets the step. range(2, 8) is 2, 3, 4, 5, 6, 7. range(0, 20, 5) jumps by 5. range(5, 0, -1) counts down and stops before 0.

main.pywhole filepython
print(list(range(2, 8)))
print(list(range(0, 20, 5)))
print(list(range(5, 0, -1)))

05Counting 1 to 100 (the off-by-one check)

To include 100, the stop must be 101. range(1, 100) would silently leave 100 out and the total would be wrong. Check both ends: first value 1, last value 100.

main.pywhole filepython
total = 0
for n in range(1, 101):
    total += n
print("Sum of 1..100:", total)      # 5050

06range is lazy

A range stores only start, stop and step, so a range of a billion numbers costs almost nothing and you can still ask for its length, test membership and index it. Printing a range shows its description, not its numbers.

main.pywhole filepython
r = range(1_000_000_000)
print(r)             # range(0, 1000000000)
print(len(r))        # 1000000000
print(500 in r)      # True
print(r[3], r[-1])   # 3 999999999

07After the loop, and zero passes

The loop variable is not erased when the loop ends: it keeps the last value. If the range is empty, the loop body never runs and the variable is never created (using it afterwards would be a NameError).

main.pywhole filepython
for k in range(3):
    pass
print("k after the loop:", k)     # 2

print(list(range(3, 3)))          # []
print(list(range(5, 1)))          # []  (stop is below start: empty)

08When you don't need the number

Use _ to say "I am not using this value". It is an ordinary variable name; the underscore only tells readers (and tools) that you ignore it.

main.pywhole filepython
for _ in range(3):
    print("Hello")

Try it yourself

  1. 1

    Predict the list

    Before running, write down list(range(10, 0, -2)). Then run it. Did you include 0? (The stop is never included.)

  2. 2

    Squares

    Write a for loop over range(1, 6) that prints each number and its square (n * n) on one line, such as 3 9. Check the first and last lines.

  3. 3

    Break the range

    Try range(3.0) and for i in 5: in a copy of the example. Read the two error messages: they tell you that range wants whole numbers and that a single number cannot be looped over.

Code & diagrams

Looping over lists, strings and ranges New tab

Change range(1, 101) to range(1, 100) and see how the total drops by exactly 100. That is an off-by-one error.

Sign in to run this example in your browser.

Expected output

Fruit: apple
Fruit: banana
Fruit: cherry
H
i
!
[0, 1, 2, 3, 4]
[2, 3, 4, 5, 6, 7]
[0, 5, 10, 15]
[5, 4, 3, 2, 1]
Sum of 1..100: 5050
range is lazy, and empty ranges New tab

A billion-number range answers instantly because Python never builds it. Try len(range(0, 10, 3)) and predict it.

Sign in to run this example in your browser.

Expected output

range(0, 1000000000)
1000000000
True
3 999999999
[]
[]
5
k after the loop: 2
What range(start, stop, step) producesdiagram
Rendering diagram…

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Give range a float

Use a decimal number in range.

main.pywhole filepython
for i in range(3.0):
    print(i)
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 1, in <module>
for i in range(3.0):
~~~~~^^^^^
TypeError: 'float' object cannot be interpreted as an integer

Break #2

Loop over a single number

Put a number where the collection should be.

main.pywhole filepython
for i in 5:
    print(i)
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 1, in <module>
for i in 5:
^
TypeError: 'int' object is not iterable

Myth vs fact

Myth

range(5) gives 1 to 5.

Fact

It gives 0, 1, 2, 3, 4: five numbers, starting at 0 and stopping before 5.

Myth

range(1_000_000) creates a list of a million numbers.

Fact

It creates a tiny object that calculates each number on demand. Only list(range(...)) builds the actual list.

Myth

The loop variable disappears when the loop ends.

Fact

It stays and holds the last value. This is handy and sometimes surprising; if the loop ran zero times, the variable was never created.

Myth

To count to n you must use a while loop.

Fact

A for loop over range is the standard way, and safer because it cannot forget the counter update.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    In Python 3, range is its own sequence type. It supports len, indexing, slicing, in and reversed, and x in range(...) is O(1) for ints because it is computed arithmetically. In Python 2, range built a real list and xrange was the lazy one; you will see xrange only in very old code.

  • ▸

    Python integers have unlimited size, so range works beyond 64 bits: range(10**30, 10**30 + 3) is fine. NumPy's np.arange is the array version for scientific code (Topic 14.1) and can take float steps, with rounding surprises.

  • ▸

    A for loop calls iter() on the collection and then next() until StopIteration (Topic 9.1). The loop variable is an ordinary name in the surrounding scope, which is why it survives the loop. The same rule means a function defined inside a loop remembers the *variable*, not its value at that pass (Topic 5.7).

  • ▸

    range is an iterable but not an iterator: you can loop over it again and again, and next(range(3)) is a TypeError. zip, enumerate, reversed and generators are iterators and are used up after one pass. Also, len() must fit a C ssize_t, so len(range(10**30)) raises OverflowError even though iterating or indexing such a range works.

  • ▸

    Never add or remove items of a list while a for loop is walking it: the loop's hidden position keeps counting, so items are skipped (Topic 4.9). Loop over a copy (for x in items[:]) or build a new list. Changing the size of a dict or set during iteration raises RuntimeError instead.

  • ▸

    The usual loop-time worry is not range but what is inside: for over a list of 10 million items does 10 million passes, each carrying Python's interpreter overhead. Heavy numeric loops are moved into NumPy (Topic 14.3), which is how AI code stays fast.

Remember this

  1. 1

    The form is for name in collection: and an indented block. On each pass Python takes the next item, gives it the name you chose (the loop variable), and runs the block. When the items run out, the loop ends by itself. The thing you loop over is a collection: a group of values kept together. You can loop over a list (for fruit in fruits, where a list is several values in order, like ["apple", "banana"]), over a string (for letter in "Hi!" visits each character) and over a range. Anything you can loop over is called an iterable; Topic 9.1 explains how that works inside.

  2. 2

    range makes counting loops. range(5) gives 0, 1, 2, 3, 4: it starts at 0 and stops before 5. range(2, 8) gives 2 to 7. range(0, 20, 5) gives 0, 5, 10, 15: the third number is the step. A negative step counts down: range(5, 0, -1) gives 5, 4, 3, 2, 1. The rule to remember: the start is included, the stop is not.

  3. 3

    That excluded end is the source of the most common loop bug, the off-by-one error. To count from 1 to 100 you need range(1, 101). The number of items in range(a, b) is b - a, so range(1, 6) has 5 items, not 6. Whenever you write a loop, check the first and the last value it will produce.

  4. 4

    range is lazy. range(1_000_000_000) does not create a billion numbers: it stores only the start, stop and step (a few dozen bytes) and calculates each number when asked. That is why len(r), 500 in r and r[3] are all instant. To see the numbers you must ask for them, for example with list(range(5)).

  5. 5

    range works only with whole numbers (range(3.0) is a TypeError, the error Python raises when you give a function the wrong kind of value). An empty range such as range(5, 1) is allowed: the loop runs zero times. After the loop ends, the loop variable still exists and keeps the last value it had (k is 2 after for k in range(3)), but it does not exist at all if the loop ran zero times.

  6. 6

    When you do not need the value, name it _ (an underscore): for _ in range(3): repeats three times. And when you have a list, loop over the items directly (for fruit in fruits), not over positions. Topic 2.6 shows how to get the position too (enumerate) without the beginner pattern range(len(x)).

Explain it without notes

01

Explain what range(2, 10, 3) produces and why 10 does not appear.

02

What is an off-by-one error? Give an example with range.

03

Why is range(1_000_000_000) cheap, and how do you see its numbers?

04

What does a for loop do when the collection is empty?

Practice

01

Print the squares of 1 to 5, one per line, with a for loop and range.

02

Add up the even numbers from 2 to 20 and print Total: 110.

03

Print a countdown 10, 7, 4, 1 using range with a negative step.

04

Count how many vowels (a, e, i, o, u) are in the text "education" and print Vowels: 5.

Trade-offs

  • ↔

    Looping over items vs over positions. Looping directly over items (for fruit in fruits) is shorter and cannot go out of range. Use positions only when you truly need the number.

  • ↔

    Lazy range vs a real list. A range saves memory and is instant; a list lets you modify it and look at all values. Convert with list() only when you need that.

Done when you can

  • Done when you can write a for loop over a list, a string and a range.

  • Done when you can state the first and last value of any range(a, b, c) and its length.

  • Done when you can spot and fix an off-by-one error.

  • Done when you can explain why a range is lazy and how to see its values.

  • Done when you can explain what happens to the loop variable after the loop.