Control Flow
Loops
A loop repeats a block until a condition fails (while) or for every item in a sequence (for). while, for, range(), iterating a string, range with start/stop/step, and nested loops — with the off-by-one, range-excludes-stop, and char-not-index traps shown live.
Suggest an editLoops — Repeating Work Without Copy-Paste
A loop is how you tell the computer to repeat work without writing it out N times. Python has two, and the thesis is to pick by what you're repeating for: while repeats as long as a condition stays true; for repeats once per item in a sequence. Most "do this to each thing" jobs are for loops; while is for "keep going until something changes." Both run an indented block, exactly like the conditionals in the last chapter.
💡 The core idea.
- A loop repeats work without copy-paste.
whilerepeats as long as a condition stays true.forrepeats once per item in a sequence.
Every output below was produced by running the code.
📘 How to read the Intuition boxes. Each one is built in three moves:
- The mechanism — what the interpreter is actually doing.
- A concrete bite — a specific, runnable way the naive assumption fails.
- The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.
Table of contents
- The
whileloop - The
forloop andrange() - Looping over a string
range(start, stop, step)- Nested loops
- Mental-model summary
- Gotcha checklist
1. The while loop
A while loop repeats its block as long as a condition is true. It checks the condition, runs the body, and checks again — over and over — until the condition becomes false.
Output:
1
2
3
doneAnalysis. count starts at 1. The condition count <= 3 is true, so the body prints count and increases it — then control loops back to the condition. This repeats for 1, 2, 3; when count reaches 4 the condition is false, the loop exits, and print("done") (outside the loop) runs.
Intuition.
Mechanism. A while re-checks its condition before every pass. For the loop to ever end, something inside the body must eventually make the condition false — here, count = count + 1.
Concrete bite. If the body doesn't move the condition toward false, the loop never stops — an infinite loop. The opposite, subtler mistake is moving it too far and stopping early, an off-by-one:
1
2We wanted 1, 2, 3 but got 1, 2: with < instead of <=, the loop exits as soon as count is 3, one iteration short.
💡 Earned rule. Guarantee progress toward the exit (update the loop variable) and check the boundary condition carefully — < vs <= is a whole iteration. The cost of getting it wrong is a hang (infinite loop — forgetting count = count + 1 here would print 1 forever) or a quiet off-by-one. If a while ever hangs, the body isn't changing what the condition tests.
2. The for loop and range()
A for loop runs its body once for each item in a sequence, binding a name to each item in turn. To repeat a fixed number of times, loop over range(n), which produces the numbers 0 up to but not including n.
Output:
0
1
2
3
4Analysis. range(5) yields 0, 1, 2, 3, 4 — five numbers, starting at 0 and stopping before 5. The name i takes each value in turn, and the body runs five times. There's no condition to maintain and no counter to increment by hand — for handles both.
Intuition.
Mechanism. for x in seq: asks seq for its items one at a time and runs the body with x bound to each. range(n) is a sequence of integers from 0 to n − 1 — n numbers, but the last is n − 1, not n.
Concrete bite. The "stops before n" rule catches everyone at first:
0
1
2range(3) is 0, 1, 2 — three values, but they're 0-based, so the largest is 2. It does not include 3, and it does not start at 1.
💡 Earned rule. range(n) gives n values, 0 to n − 1. To count 1 to n, use range(1, n + 1) (next section). The cost of zero-based, stop-exclusive ranges is the constant temptation to off-by-one — but it's the same rule as string indexing (Tutorial 4) and slicing, so it pays to internalise once.
3. Looping over a string
for works on any sequence, and a string is a sequence of characters (Tutorial 4). Looping over a string gives you each character, in order.
Output:
c
a
tAnalysis. The loop bound letter to "c", then "a", then "t" — the characters themselves, not their positions. This is the natural way to process text one character at a time, with no indexing needed.
Intuition.
Mechanism. Iterating a string yields its characters directly. The loop variable holds the character, not the index — Python's for is a "for each item" loop, not a "for each index" loop like in some other languages.
Concrete bite. Expecting the loop variable to be an index (0, 1, 2) and using it to subscript the string fails:
Traceback (most recent call last):
File "/w/main.py", line 3, in <module>
print("cat"[i])
~~~~~^^^
TypeError: string indices must be integers, not 'str'i is "c", "a", "t" — characters — so "cat"[i] tries to index with a string and raises TypeError. The loop already gave you the character; there's nothing to index.
💡 Earned rule. Loop directly over items (for letter in word) and use the item. The cost/boundary: when you genuinely need the position too, ask for both with enumerate — for i, letter in enumerate(word) gives index and character — rather than looping over indices and re-indexing.
4. range(start, stop, step)
range takes up to three arguments: where to start, where to stop (exclusive), and the step between values. A negative step counts down.
Output:
0
2
4
6
8Analysis. range(0, 10, 2) starts at 0, steps by 2, and stops before 10: 0, 2, 4, 6, 8. The stop is exclusive here too, so 10 itself never appears.
Intuition.
Mechanism. range(start, stop, step) yields start, start + step, start + 2·step, … and halts as soon as it reaches or passes stop (which is never included). A negative step walks downward.
Concrete bite. A negative step gives a countdown — and shows the stop-exclusive rule from the other direction:
3
2
1
liftoffrange(3, 0, -1) counts 3, 2, 1 — it stops before 0, so 0 isn't printed. To count down to and include 0, you'd write range(3, -1, -1).
💡 Earned rule. Read range(a, b, s) as "from a, step s, stop before b." The cost is that the exclusive stop flips meaning with a negative step (it's now a lower bound you stop before), so countdowns need a stop one past where you want to end.
5. Nested loops
A loop can contain another loop. For every pass of the outer loop, the entire inner loop runs. This is how you walk a grid, a table, or all pairs.
Output:
0 0
0 1
0 2
1 0
1 1
1 2Analysis. The outer loop runs for row = 0, then row = 1. For each of those, the inner loop runs fully (col = 0, 1, 2). So the body runs 2 × 3 = 6 times, producing every (row, col) pair. The inner loop completes before the outer loop advances.
Intuition. Mechanism. Nesting multiplies iterations: the inner loop restarts and runs to completion on every single pass of the outer loop. Total body executions = outer count × inner count.
Concrete bite. That multiplication is easy to underestimate — the output above is six lines from loops of "2" and "3," not five. With larger ranges it compounds fast: two nested range(1000) loops run the body a million times. A triple nest of 1000 is a billion.
💡 Earned rule. Reach for nested loops to cover every combination (grids, pairs), but watch the multiplied cost — it's the difference between instant and frozen. The boundary: when nesting gets deep or slow, that's the signal to look for a better data structure or algorithm, a theme Performance returns to in Tier 5.
6. Mental-model summary
| Principle | Consequence |
|---|---|
while cond: repeats until cond is false |
The body must move toward the exit, or it loops forever |
for x in seq: runs once per item, binding x |
No manual counter; x is the item, not its index |
range(n) is 0 … n−1 (stop-exclusive) |
range(3) is 0,1,2; count 1..n with range(1, n+1) |
| Iterating a string yields characters | Use the character directly; enumerate if you also need the index |
range(a, b, s) steps by s, stops before b |
Negative s counts down; b is a bound you stop before |
| Nested loops multiply: outer × inner runs | Watch the cost — it compounds fast with size |
7. Gotcha checklist
- Loop never ends (hang) → the
whilebody doesn't change what the condition tests; ensure progress toward false. - Loop runs one too few/many times → off-by-one; check
<vs<=, and thatrangestop is exclusive. range(n)didn't includen/ started at 0 → by design; userange(1, n+1)for1..n.TypeError: string indices must be integers→ theforvariable is the item (a character), not an index; use it directly orenumerate.- Nested loop is unexpectedly slow → iterations multiply; reconsider the structure for large sizes.
🧪 Predict, then check. Without running it, write down everything this prints: for i in range(1, 4): with a nested for j in range(1, 4): whose body is print(i, "x", j, "=", i * j). How many lines? Then change the outer to range(1, 10) and (don't run) estimate how many lines a full times-table would be. Build the 1–3 version and confirm your line-by-line prediction.
Your Turn
Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.