A trace table is a tool for reading code without losing track of what is happening. You make a column for each variable and a row for each line. As you work through the program, you fill in what changes. By the end, you know exactly what every variable contained at every moment — and you can predict the output with confidence.
This article is part of the Code Reading, Tracing & Prediction section. If you have not read How to Read Code yet, start there. Trace tables are the main technique used throughout this section, so understanding them well pays off across every other article.
| Level | What it covers | When to read it |
|---|---|---|
| Basic idea | What a trace table is and how to fill one in | Right now. |
| At a deeper level | How to handle conditionals and output in a trace table; common mistakes | Once you have filled in your first table on your own. |
| At the deepest level | What trace tables reveal about memory and state; when they become unwieldy | When you want to understand what the table is really modelling. |
1 What a Trace Table Is
When you trace a program in your head, you have to remember what every variable contains while also reading forward. That is too much to hold in memory at once. A trace table solves this by moving the remembering onto paper.
The structure is simple:
- One column for the line of code being executed
- One column for each variable in the program
- One column for output — what gets printed
- One row for each line that does something
You fill in cells as you go. When a variable changes, you write its new value. When a variable does not change on a given line, you leave that cell blank (or copy the previous value — either convention works, as long as you are consistent).
Here is what a trace table looks like in practice. Take this program:
x = 5 y = 2 x = x + y y = x - 1 print(y)
Set up the table with a column for the line, a column for each variable (x and y), and an output column. Then work through the program one line at a time.
| Line | x | y | Output |
|---|---|---|---|
x = 5 | 5 | ||
y = 2 | 2 | ||
x = x + y | 7 | ||
y = x - 1 | 6 | ||
print(y) | 6 |
Reading the Output column top to bottom gives you the program's complete output: 6.
Notice the third row: x = x + y reads the current value of x (5) and the current value of y (2), adds them, and stores the result back in x. The table makes this visible — you look left at the previous rows to find the current values, then write the new value in the current row.
To find what a variable contains at any point in the program, look at that variable's column and scan upward from the current row until you find the most recent non-empty cell. That is the current value. This is exactly what the computer does — it always uses the most recently stored value.
A trace table is a hand-made record of a program's state at each step. In computer science, state means the complete set of values stored in memory at a given moment. Each row in the trace table is a snapshot of state after one line executes.
This is exactly what a debugger does automatically. When you pause execution in VS Code's debugger and look at the Variables panel, you are reading the program's current state — the same information your trace table records manually. Learning to build trace tables by hand trains you to think in terms of state, which makes debuggers far easier to use when you reach them.
2 How to Set Up a Trace Table
Before you start tracing, scan the whole program and identify every variable. Make one column per variable. If you miss a variable and need to add it later, that is fine — but scanning first saves time.
The steps:
- Read the whole program once without tracing.
- List every variable name you see.
- Draw the table: Line | variable 1 | variable 2 | ... | Output
- Start at line 1 and work downward, one row per executable line.
- Only write in a cell when that variable's value changes on that line, or when there is output.
Comments (# like this) and blank lines do not change any values, so they do not get rows.
Two conventions exist for cells where a variable does not change:
| Convention | What you write in an unchanged cell | Pros and cons |
|---|---|---|
| Leave blank | (nothing) | Faster to fill in. Changes stand out clearly. Requires scanning upward to find the current value. |
| Carry forward | Repeat the previous value in every row | The current value is always visible in the same row. Takes longer to fill in and can feel repetitive. |
In this course, either convention is acceptable. The leave-blank approach is faster and is used in the examples throughout this article. Use whichever helps you stay accurate.
A common mistake is to create one row for each variable rather than one row for each line of code. This breaks the table — you lose track of the order in which things happened. Rows represent time (the order of execution). Columns represent space (the variables in memory). Keep those two ideas separate.
The choice between "leave blank" and "carry forward" reflects two different ways of modelling memory. The carry-forward approach models memory as something that always has a value — which is accurate. Every variable always contains something after it is first assigned. The leave-blank approach highlights only the moments of change — which is more useful for spotting bugs, because bugs are usually caused by a value changing at the wrong time or not changing when it should.
Experienced programmers reading someone else's code often do a mental version of the leave-blank approach: they track only the lines that matter, skipping over lines that simply use a value without modifying it.
3 Handling Conditionals in a Trace Table
When your program has an if statement, you need to decide which branch runs — and only trace the lines inside that branch. The lines in the branch that does not run are skipped entirely. They do not get rows in the table.
To decide which branch runs, look at the condition and check it against the current values in your table.
Here is a program with a conditional:
score = 72
grade = "F"
if score >= 60:
grade = "Pass"
else:
grade = "Fail"
print(grade)| Line | score | grade | Output |
|---|---|---|---|
score = 72 | 72 | ||
grade = "F" | "F" | ||
if score >= 60: | Condition: 72 >= 60 → True. Take the if branch. | ||
grade = "Pass" | "Pass" | ||
print(grade) | Pass | ||
The condition row does not change any variable. It makes a decision. Write out the condition, evaluate it using the current values, and note which branch you are taking. Then skip all the rows inside the branch you are not taking.
In this example, because score >= 60 is True, the line grade = "Fail" inside the else block never runs. It does not appear in the table at all.
Do not evaluate the condition silently in your head and skip straight to the branch. Write it out in the table — even if just as a note in a merged cell. This forces you to use the actual current values rather than the values you expect them to be. Most conditional tracing errors happen because the programmer used an assumed value rather than looking up the current one.
When a condition is evaluated, Python constructs a temporary Boolean value — True or False — that is not stored in any named variable. The condition row in your trace table captures this temporary evaluation. In a real interpreter, this temporary value exists in the CPU for the duration of the branch decision and is then discarded.
Complex conditions using and, or, and not are evaluated according to short-circuit rules: Python stops evaluating as soon as the result is certain. For and, if the left side is False, the right side is never checked. For or, if the left side is True, the right side is never checked. When tracing complex conditions, evaluate left to right and note where evaluation stops.
4 Handling Loops in a Trace Table
Loops repeat lines of code. In a trace table, each time through the loop gets its own set of rows — even though it is the same lines of code repeating. A loop that runs three times will produce three groups of rows in your table.
This is the part where trace tables get longer. That length is the point: it forces you to see exactly what is happening on each repetition, rather than glossing over it.
Here is a short loop:
total = 0
for i in range(3):
total = total + i
print(total)| Line | total | i | Output |
|---|---|---|---|
total = 0 | 0 | ||
for i in range(3): — iteration 1 | 0 | ||
total = total + i | 0 | ||
for i in range(3): — iteration 2 | 1 | ||
total = total + i | 1 | ||
for i in range(3): — iteration 3 | 2 | ||
total = total + i | 3 | ||
print(total) | 3 |
A few things to notice:
range(3)produces the values 0, 1, 2 — not 1, 2, 3. The loop runs three times, andistarts at 0.- On iteration 1,
total = total + iis0 + 0 = 0. The value does not change, but it is still worth writing in the table to confirm your reasoning. - The final value of
totalis 3, which is 0 + 1 + 2.
When the loop header appears multiple times in your table, add a label — "iteration 1", "iteration 2" — to keep rows from blurring together. This is especially important with while loops, where the condition is re-evaluated each time and you need to check it carefully on every iteration.
The trace table for a loop grows linearly with the number of iterations. For a loop that runs 100 times, a full trace table would have hundreds of rows — impractical to write by hand. In practice, you trace enough iterations to confirm your understanding of the pattern (usually two or three), then reason about the remaining iterations abstractly.
This is the same approach used in mathematical induction: show that the base case is correct, show that one step follows correctly from the previous, then conclude that all subsequent steps work the same way. For a loop that accumulates a total, tracing three iterations and seeing the pattern is sufficient to predict what the 100th iteration would produce — provided the loop body does not contain conditional logic that behaves differently in edge cases.
5 Common Mistakes
These are the mistakes that appear most often when students fill in trace tables for the first time. Knowing what they are before you make them saves time.
| Mistake | What it looks like | How to avoid it |
|---|---|---|
| Using an old value | You calculate x + y using the value of y from two rows ago instead of the most recent one | Always scan upward in the column to find the most recent non-empty cell before using a variable's value |
| Writing the expression instead of the result | You write x + y in the cell instead of the computed value, such as 7 | Always evaluate the expression and write the final value. The table records what the computer stores — not the calculation it performed to get there. |
| Tracing skipped branches | You include rows for code inside an else block even when the if condition was True | When a condition is True, the else block does not run. Draw a line through those rows or leave them out entirely. |
| Forgetting that range(n) starts at 0 | You set i = 1 on the first iteration of for i in range(3) | range(3) produces 0, 1, 2. Always write out the sequence before tracing the loop. |
| Stopping the loop too early or too late | You give a loop three iterations when it should have four, or vice versa | Evaluate the loop condition before each iteration, including the one that finally fails. The loop stops when the condition becomes False — write that out explicitly. |
The most damaging mistake is not any of the ones above — it is not checking your trace against the actual output. A trace table is only useful if you run the program after completing it and compare the Output column to what Python actually printed. If they differ, the discrepancy tells you exactly which line your mental model got wrong.
When your prediction is wrong, resist the urge to immediately change the table to match the output. Instead, re-run the trace mentally from the start and find the specific line where your reasoning diverged from the program's behaviour. That line is where your understanding has a gap. Fixing the table without finding the gap means the gap is still there, waiting to cause the next wrong prediction.
6 A Full Worked Example
Here is a complete worked trace of a program that uses variables, a loop, and a conditional. Read through the table row by row before looking at the output.
count = 0
numbers = [4, 7, 2, 9]
for n in numbers:
if n > 5:
count = count + 1
print(count)The variables are count, n, and the list numbers. Lists do not change in this program, so the table only needs columns for count and n.
| Line | count | n | Output |
|---|---|---|---|
count = 0 | 0 | ||
for n in numbers: — iteration 1 | 4 | ||
if n > 5: | 4 > 5 → False. Skip the if block. | ||
for n in numbers: — iteration 2 | 7 | ||
if n > 5: | 7 > 5 → True. Enter the if block. | ||
count = count + 1 | 1 | ||
for n in numbers: — iteration 3 | 2 | ||
if n > 5: | 2 > 5 → False. Skip the if block. | ||
for n in numbers: — iteration 4 | 9 | ||
if n > 5: | 9 > 5 → True. Enter the if block. | ||
count = count + 1 | 2 | ||
print(count) | 2 | ||
The output is 2. The program counted how many numbers in the list were greater than 5 — and there were two of them: 7 and 9.
Notice how the table makes the program's purpose visible. Before tracing, the program might look like a collection of syntax. After tracing, you can see what it is doing: it loops through a list and counts the values that meet a condition. The table reveals the intent, not just the mechanics.
This is one of the underrated benefits of trace tables. They do not just help you predict output — they help you understand what a program is for. When you are reading someone else's code, or returning to your own code after a break, a short trace often explains the logic faster than reading the variable names and comments.
- In your own words, what is a trace table and why is it useful?
Complete a trace table for this program. Write out every row.
a = 8 b = 3 a = a - b b = a + 1 print(b)
Complete a trace table for this program. Show the condition evaluation row each time the
ifis reached.temperature = 18 message = "unknown" if temperature >= 20: message = "warm" else: message = "cool" print(message)Complete a trace table for this program. Label each loop iteration.
result = 1 for i in range(1, 4): result = result * i print(result)What does this program calculate? Can you name it?
- A classmate traces the program in question 4 and writes
i = 1, 2, 3, 4for the loop iterations. What mistake have they made, and how would you explain the correct behaviour ofrange(1, 4)? - After completing a trace table, you run the program and the output is different from what your table predicted. Describe the process you would use to find where your trace went wrong.