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
forloop 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
fruitinfor 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
stopis 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.
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print("Fruit:", fruit)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.
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.
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.
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.
total = 0
for n in range(1, 101):
total += n
print("Sum of 1..100:", total) # 505006range 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.
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 99999999907After 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).
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.
for _ in range(3):
print("Hello")Try it yourself
- 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
Squares
Write a
forloop overrange(1, 6)that prints each number and its square (n * n) on one line, such as3 9. Check the first and last lines. - 3
Break the range
Try
range(3.0)andfor i in 5:in a copy of the example. Read the two error messages: they tell you thatrangewants whole numbers and that a single number cannot be looped over.
Code & diagrams
Change range(1, 101) to range(1, 100) and see how the total drops by exactly 100. That is an off-by-one error.
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: 5050A billion-number range answers instantly because Python never builds it. Try len(range(0, 10, 3)) and predict it.
Expected output
range(0, 1000000000)
1000000000
True
3 999999999
[]
[]
5
k after the loop: 2Break 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.
for i in range(3.0):
print(i)Break #2
Loop over a single number
Put a number where the collection should be.
for i in 5:
print(i)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,
rangeis its own sequence type. It supportslen, indexing, slicing,inandreversed, andx in range(...)is O(1) for ints because it is computed arithmetically. In Python 2,rangebuilt a real list andxrangewas the lazy one; you will seexrangeonly in very old code. - ▸
Python integers have unlimited size, so
rangeworks beyond 64 bits:range(10**30, 10**30 + 3)is fine. NumPy'snp.arangeis the array version for scientific code (Topic 14.1) and can take float steps, with rounding surprises. - ▸
A
forloop callsiter()on the collection and thennext()untilStopIteration(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). - ▸
rangeis an iterable but not an iterator: you can loop over it again and again, andnext(range(3))is aTypeError.zip,enumerate,reversedand generators are iterators and are used up after one pass. Also,len()must fit a Cssize_t, solen(range(10**30))raisesOverflowErroreven though iterating or indexing such a range works. - ▸
Never add or remove items of a list while a
forloop 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 raisesRuntimeErrorinstead. - ▸
The usual loop-time worry is not
rangebut what is inside:forover 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
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 arange. Anything you can loop over is called an iterable; Topic 9.1 explains how that works inside. - 2
rangemakes counting loops.range(5)gives0, 1, 2, 3, 4: it starts at 0 and stops before 5.range(2, 8)gives2to7.range(0, 20, 5)gives0, 5, 10, 15: the third number is the step. A negative step counts down:range(5, 0, -1)gives5, 4, 3, 2, 1. The rule to remember: the start is included, the stop is not. - 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 inrange(a, b)isb - a, sorange(1, 6)has 5 items, not 6. Whenever you write a loop, check the first and the last value it will produce. - 4
rangeis 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 whylen(r),500 in randr[3]are all instant. To see the numbers you must ask for them, for example withlist(range(5)). - 5
rangeworks only with whole numbers (range(3.0)is aTypeError, the error Python raises when you give a function the wrong kind of value). An empty range such asrange(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 (kis2afterfor k in range(3)), but it does not exist at all if the loop ran zero times. - 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 patternrange(len(x)).
Explain it without notes
Explain what range(2, 10, 3) produces and why 10 does not appear.
What is an off-by-one error? Give an example with range.
Why is range(1_000_000_000) cheap, and how do you see its numbers?
What does a for loop do when the collection is empty?
Practice
Print the squares of 1 to 5, one per line, with a for loop and range.
Add up the even numbers from 2 to 20 and print Total: 110.
Print a countdown 10, 7, 4, 1 using range with a negative step.
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
forloop 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.