Loops are where beginner mental models break most often. A loop does not just "repeat" — it repeats a specific number of times, in a specific order, with a specific stopping condition. Tracing a loop means working through every single iteration by hand, checking the condition each time, and recording what changes. The trace table makes that process visible and accurate.
This article is part of the Code Reading, Tracing & Prediction section. It assumes you have already read Trace Tables. If you have not, read that article first — the techniques here build directly on it.
Python has two kinds of loop. This article covers both.
| Loop type | When it is used | How it stops |
|---|---|---|
for | When you know in advance how many times to repeat, or when you are working through a sequence (a list, a range, a string) | When the sequence runs out |
while | When the number of repetitions depends on something that changes during the loop | When the condition becomes False |
| Level | What it covers | When to read it |
|---|---|---|
| Basic idea | The core tracing process for for and while loops | Right now. |
| At a deeper level | Off-by-one errors, infinite loops, and loops that modify the thing they are iterating over | Once you have completed a trace of each loop type on your own. |
| At the deepest level | How Python implements loops internally; what the trace table is really modelling | When the basic approach feels solid and you want to understand what is happening underneath. |
1 Tracing a for Loop
A for loop works through a sequence — a list, a range of numbers, or a string — one item at a time. On each iteration, the loop variable takes the next value from the sequence. When the sequence is exhausted, the loop stops.
To trace a for loop:
- Write out the sequence before you start. For
range(5), write: 0, 1, 2, 3, 4. For a list like[10, 20, 30], write the list values in order. Do not rely on your head to track this. - Create a row for the loop header on each iteration, showing the new value of the loop variable.
- Create rows for each line inside the loop body.
- Repeat steps 2 and 3 for every item in the sequence.
- When the sequence is empty, the loop ends. Continue with the line after the loop.
Here is a for loop to trace:
total = 0
values = [3, 6, 2, 8]
for v in values:
total = total + v
print(total)The sequence is the list [3, 6, 2, 8]. There are four items, so the loop runs exactly four times. Write that out before starting the table: v will take the values 3, then 6, then 2, then 8.
| Line | total | v | Output |
|---|---|---|---|
total = 0 | 0 | ||
for v in values: — iteration 1 | 3 | ||
total = total + v | 3 | ||
for v in values: — iteration 2 | 6 | ||
total = total + v | 9 | ||
for v in values: — iteration 3 | 2 | ||
total = total + v | 11 | ||
for v in values: — iteration 4 | 8 | ||
total = total + v | 19 | ||
print(total) | 19 |
The output is 19, which is 3 + 6 + 2 + 8. The table makes it clear that total accumulates — it does not reset to zero on each iteration, it keeps the running sum from the previous iteration.
The loop variable (v above) is updated by the loop header, not inside the loop body. When you write the iteration row, the new value of v goes in the header row — before any of the body lines run. This ordering matters: the body uses the new value of v that was just set.
Under the hood, a for loop in Python works through an iterator — an object that knows its current position in a sequence and can produce the next item on demand. When Python executes for v in values, it calls iter(values) to create an iterator, then calls next() on that iterator at the start of each iteration. When next() raises a StopIteration exception (because the sequence is exhausted), the loop ends.
This mechanism means that any object implementing the iterator protocol can be used in a for loop — lists, strings, ranges, files, database query results, and custom objects alike. The loop does not care what the sequence is, only that it can be iterated. This is why for line in file works the same way as for item in list: they both use the same underlying protocol.
2 Understanding range()
range() generates a sequence of integers. It is the most common way to control how many times a for loop runs. The key thing to know before tracing any loop that uses range() is exactly which numbers it produces.
| Call | Sequence produced | Number of iterations |
|---|---|---|
range(4) | 0, 1, 2, 3 | 4 |
range(1, 5) | 1, 2, 3, 4 | 4 |
range(2, 8, 2) | 2, 4, 6 | 3 |
range(5, 0, -1) | 5, 4, 3, 2, 1 | 5 |
range(3, 3) | (nothing) | 0 — the loop body never runs |
The rules: range(stop) starts at 0 and goes up to — but not including — stop. range(start, stop) starts at start and goes up to but not including stop. range(start, stop, step) adds step each time instead of 1.
Before tracing any loop that uses range(), write the sequence out in full. Do not try to remember it while also tracing the loop body.
The stop value being excluded — rather than included — is the most common source of off-by-one errors for beginners. Here is a concrete example of how this creates a bug:
# Intended: print 1 through 5
for i in range(1, 5):
print(i)Tracing the sequence: range(1, 5) produces 1, 2, 3, 4. The number 5 is not included. The loop prints 1, 2, 3, 4 — not 5. To print 1 through 5, the call should be range(1, 6).
This behaviour is deliberate and has a practical advantage: range(n) always produces exactly n items, starting from 0. This makes it straightforward to use the loop variable as an index into a list of length n — the index values 0 through n-1 match the valid positions in the list exactly.
For any for loop using range(), write the full sequence as a comment or note before filling in the table. "range(2, 7) → 2, 3, 4, 5, 6. Five iterations." This takes ten seconds and prevents most range-related tracing errors.
range() does not actually generate a list of numbers in memory. It creates a lazy sequence object that computes each value on demand. range(1000000) uses almost no memory — it just remembers the start, stop, and step values, and calculates the next integer each time the loop asks for one. This is why range() can represent very large sequences without performance issues.
If you want to see the actual list a range produces, you can convert it: list(range(1, 6)) returns [1, 2, 3, 4, 5]. This is a useful debugging technique when you are not sure what a range() call will produce.
3 Tracing a while Loop
A while loop keeps running as long as its condition is True. Unlike a for loop, there is no pre-set sequence — the loop continues until something inside the loop body changes the condition to False.
To trace a while loop:
- Before the first iteration, check the condition using the current variable values. If it is already
False, the loop body never runs at all. - If the condition is
True, run the loop body line by line and record the changes. - After completing the loop body, go back to the condition and check it again with the updated values.
- Repeat until the condition is
False. Then continue with the line after the loop.
The condition check must appear in your trace table at the start of every iteration — including the final one where it becomes False.
Here is a while loop to trace:
n = 1
while n < 20:
n = n * 2
print(n)| Line | n | Output |
|---|---|---|
n = 1 | 1 | |
while n < 20: — check 1 | 1 < 20 → True. Enter loop body. | |
n = n * 2 | 2 | |
while n < 20: — check 2 | 2 < 20 → True. Enter loop body. | |
n = n * 2 | 4 | |
while n < 20: — check 3 | 4 < 20 → True. Enter loop body. | |
n = n * 2 | 8 | |
while n < 20: — check 4 | 8 < 20 → True. Enter loop body. | |
n = n * 2 | 16 | |
while n < 20: — check 5 | 16 < 20 → True. Enter loop body. | |
n = n * 2 | 32 | |
while n < 20: — check 6 | 32 < 20 → False. Exit loop. | |
print(n) | 32 | |
The output is 32. Notice that the loop ran five times — not until n reached exactly 20, but until n first exceeded 20. The condition is checked before each iteration, so once n is 32 and the check fails, the body does not run again.
A common mistake is to assume the loop stops the moment the condition would become false, mid-body. That is not how it works. Python completes the entire loop body, then returns to the condition check. If the condition is now false, the loop ends. If the condition becomes false partway through the body, the rest of the body still runs before the check happens.
The while loop is the more fundamental of the two loop types. In theory, any for loop can be rewritten as a while loop. The for loop is a convenience — it handles the bookkeeping of advancing through a sequence so you do not have to write it yourself.
The equivalent of for i in range(3) written as a while loop:
i = 0
while i < 3:
# loop body here
i = i + 1Three things must be managed manually: initialising the counter before the loop, checking the condition, and incrementing the counter at the end of the body. Forgetting the increment is the most common cause of infinite loops in beginner code. The for loop handles all three automatically, which is why it is preferred whenever you are iterating over a known sequence.
4 Off-By-One Errors
An off-by-one error is when a loop runs one time too many or one time too few. It is one of the most common bugs in programming — experienced programmers make it regularly. The trace table is the most reliable way to catch it, because it forces you to see exactly how many iterations actually happen.
Off-by-one errors usually come from one of three places:
- Using
<when you meant<=, or vice versa - Starting a range at 1 when it should start at 0, or at 0 when it should start at 1
- Using the wrong stop value in
range()
Here are two programs that look almost identical but behave differently. Trace both and count the iterations.
Version A:
count = 0
n = 1
while n < 5:
count = count + 1
n = n + 1
print(count)Version B:
count = 0
n = 1
while n <= 5:
count = count + 1
n = n + 1
print(count)Tracing Version A — the condition is n < 5:
| Line | count | n | Output |
|---|---|---|---|
count = 0 | 0 | ||
n = 1 | 1 | ||
while n < 5: — check 1 | 1 < 5 → True | ||
count = count + 1 | 1 | ||
n = n + 1 | 2 | ||
while n < 5: — check 2 | 2 < 5 → True | ||
count = count + 1 | 2 | ||
n = n + 1 | 3 | ||
while n < 5: — check 3 | 3 < 5 → True | ||
count = count + 1 | 3 | ||
n = n + 1 | 4 | ||
while n < 5: — check 4 | 4 < 5 → True | ||
count = count + 1 | 4 | ||
n = n + 1 | 5 | ||
while n < 5: — check 5 | 5 < 5 → False. Exit loop. | ||
print(count) | 4 | ||
Version A prints 4. The loop ran when n was 1, 2, 3, and 4 — stopping before n reached 5.
Version B uses n <= 5 instead of n < 5. The only difference is that check 5 now evaluates 5 <= 5 → True, so the loop body runs one more time — making count = 5 and n = 6 before the sixth check fails. Version B prints 5.
One character difference. One extra iteration. The trace table makes this visible; guessing from the code alone is unreliable.
When checking for off-by-one errors, pay special attention to the last one or two iterations. Trace the condition check when the variable is at the boundary value — the value where the condition is about to change from True to False. That is where the error always hides.
Off-by-one errors are so common that they have their own informal name among programmers: fencepost errors. The name comes from a counting puzzle: if you build a fence 10 metres long with posts every metre, how many posts do you need? The intuitive answer is 10, but the correct answer is 11 — because you need a post at both ends, not just one. Whether to count the endpoints is the same question as whether to use < or <=.
Dijkstra, one of the most influential computer scientists of the 20th century, wrote a well-known argument for why sequences should always be zero-indexed and the stop value always excluded — the convention Python follows. His reasoning: it produces the most consistent behaviour across edge cases, including empty sequences and sequences starting at zero. The convention feels arbitrary until you have made enough off-by-one errors to appreciate why consistency matters.
5 Infinite Loops
An infinite loop is a loop whose condition never becomes False. The loop runs forever — or until you force the program to stop. In VS Code or the terminal, you can stop a runaway program by pressing Ctrl+C.
Infinite loops are almost always bugs. They happen when the loop body does not actually change the variable that the condition depends on — so the condition stays True forever.
# Infinite loop — spot the bug
n = 1
while n < 10:
print(n)
# n never changes!The value of n is always 1. The condition 1 < 10 is always True. The loop never stops.
Trace tables catch infinite loops before you run the program. Here is the diagnostic process: trace two or three iterations. If the variable the condition depends on has not changed — or has changed in a direction that will never make the condition False — you have an infinite loop.
Here is a subtler example where the loop variable changes but in the wrong direction:
n = 10
while n > 0:
n = n + 1
print(n)The condition is n > 0, which starts True. The loop body adds 1 to n each time. After two iterations: n is 12. After three: n is 13. The condition n > 0 will never become False, because n is growing, not shrinking. This is an infinite loop.
Tracing two iterations reveals it immediately:
| Line | n | Output |
|---|---|---|
n = 10 | 10 | |
while n > 0: — check 1 | 10 > 0 → True | |
n = n + 1 | 11 | |
while n > 0: — check 2 | 11 > 0 → True. n is increasing, not decreasing. This loop will not stop. | |
After two iterations it is already clear: n started at 10, increased to 11, and the condition became more True, not less. Stop here. The bug is that the loop body should subtract 1, not add 1.
After each iteration of a while loop, ask yourself: is the variable the condition depends on getting closer to making the condition False? If the answer is no — or if the variable is moving in the wrong direction — you have found an infinite loop before it runs.
Whether a loop terminates is not always determinable just by looking at it. This is related to one of the most famous results in theoretical computer science: the halting problem, proved by Alan Turing in 1936. Turing showed that there is no general algorithm that can examine any arbitrary program and decide, for every possible input, whether that program will eventually stop or run forever. The problem is undecidable in the general case.
In practice, most loops programmers write are simple enough that termination is obvious from a trace. The halting problem only becomes relevant for programs of arbitrary complexity — but it is a useful reminder that "will this loop stop?" is a deeper question than it first appears.
6 A Full Worked Example — Loop with Accumulator and Conditional
Here is a complete trace of a program that combines a for loop, an accumulator variable, and a conditional. Read the program first, predict the output, then check your prediction against the table.
scores = [45, 72, 61, 38, 90]
passing = 0
failing = 0
for s in scores:
if s >= 60:
passing = passing + 1
else:
failing = failing + 1
print("Pass:", passing)
print("Fail:", failing)The sequence is the list [45, 72, 61, 38, 90] — five iterations. Variables to track: s, passing, failing.
| Line | s | passing | failing | Output |
|---|---|---|---|---|
passing = 0 | 0 | |||
failing = 0 | 0 | |||
for s in scores: — iteration 1 | 45 | |||
if s >= 60: | 45 >= 60 → False. Take else branch. | |||
failing = failing + 1 | 1 | |||
for s in scores: — iteration 2 | 72 | |||
if s >= 60: | 72 >= 60 → True. Take if branch. | |||
passing = passing + 1 | 1 | |||
for s in scores: — iteration 3 | 61 | |||
if s >= 60: | 61 >= 60 → True. Take if branch. | |||
passing = passing + 1 | 2 | |||
for s in scores: — iteration 4 | 38 | |||
if s >= 60: | 38 >= 60 → False. Take else branch. | |||
failing = failing + 1 | 2 | |||
for s in scores: — iteration 5 | 90 | |||
if s >= 60: | 90 >= 60 → True. Take if branch. | |||
passing = passing + 1 | 3 | |||
print("Pass:", passing) | Pass: 3 | |||
print("Fail:", failing) | Fail: 2 | |||
The output is:
Pass: 3 Fail: 2
The scores 72, 61, and 90 met the passing condition. The scores 45 and 38 did not. The trace confirms this exactly — and also confirms that neither counter was ever reset or overwritten accidentally.
- Write out the sequence produced by each of these
range()calls before tracing anything.range(6)range(2, 7)range(0, 10, 3)range(5, 0, -1)
Complete a trace table for this program. How many times does the loop body run?
total = 0 for i in range(1, 5): total = total + i print(total)Complete a trace table for this
whileloop. Include a condition check row before every iteration, including the one that exits the loop.x = 100 while x > 10: x = x // 2 print(x)A student writes this loop intending to print the numbers 1 through 10. Trace it and explain exactly what it prints instead. Then fix the bug.
for i in range(10): print(i)Without running this program, identify whether it contains an infinite loop. Trace two iterations to support your answer.
n = 5 while n != 0: n = n - 2 print(n)Trace this program and predict the output. Then explain in one sentence what the program calculates.
numbers = [8, 3, 11, 5, 7] largest = numbers[0] for n in numbers: if n > largest: largest = n print(largest)