Tracing Control-Flow-Heavy Programs
Learning Objectives
- You can trace a program with conditionals using a state table that shows which branch was taken.
- You can trace a
fororwhileloop using a row-per-iteration table. - You can use debug prints inside loops without drowning in output.
- You can identify the line in a trace where program behavior diverged from what you expected.
In Part 1, tracing was a straightforward exercise: walk down the program line by line, write down what each variable holds, and read off what gets printed. The programs were short and linear. Each line ran exactly once.
In this part, programs do not work that way. if statements pick a path. for and while loops run the same lines many times. The same skill applies — trace line by line — but the shape of the trace changes.
This chapter shows how to extend the trace tables from Part 1 to handle branching and looping code.
Tracing Matters More Now
Linear code has one path. If you trace it once, you have traced all of it.
Code with branches has many paths. The program that ran when you tested it took one of them. The other paths only get exercised if you give the program different input. Tracing is how you check those paths without having to run the program again and again.
Code with loops compounds this. Each iteration is a small reuse of the same lines, and bugs often hide in either the very first iteration, the very last iteration, or the special case where the loop body runs zero times. Tracing forces you to look at each iteration separately.
The good news is that the underlying habit is the same: predict before running, run, compare. This chapter just sharpens the trace notation to fit the new shapes.
Tracing an if/elif/else
For branching code, a useful extension to the Part 1 trace table is a column showing which branch was taken.
minutes = 45
if minutes < 15: response = "Short."elif minutes <= 90: response = "Solid."else: response = "Long."
print(response)| Step | Line | Branch | State after | Output |
|---|---|---|---|---|
| 1 | minutes = 45 | — | minutes = 45 | (nothing) |
| 2 | if minutes < 15: | not taken | minutes = 45 | (nothing) |
| 3 | elif minutes <= 90: | taken | minutes = 45, response = "Solid." | (nothing) |
| 4 | print(response) | — | unchanged | Solid. |
The “branch” column makes it explicit which path the program took. For chains with several elifs, this is the difference between guessing at the behavior and actually knowing it.
A useful trick when tracing branching code: trace the same program twice, with two different inputs that take different branches. If both traces look right, you have evidence that the branches do what you think they do.
Complete a branch trace
0 / 10 points
Trace this program:
minutes = 12
if minutes < 10: label = "tiny"elif minutes < 30: label = "short"else: label = "long"
print(label)State what happens for each branch, the final value of label, and the output.
Use this structure:
minutes < 10: ...minutes < 30: ...else: ...label: ...output: ...Tracing a for Loop
For loops, the natural trace shape is one row per iteration.
minutes_list = [45, 30, 60]total = 0
for minutes in minutes_list: total += minutes
print(total)| Iteration | minutes | total after | Output |
|---|---|---|---|
| (before loop) | — | 0 | (nothing) |
| 1 | 45 | 45 | (nothing) |
| 2 | 30 | 75 | (nothing) |
| 3 | 60 | 135 | (nothing) |
| (after loop) | — | 135 | 135 |
The “(before loop)” and “(after loop)” rows are deliberately included. The before row reminds you that the accumulator starts at 0. The after row reminds you that anything outside the loop runs once at the end. Most bugs in accumulator loops are about one of these two rows being wrong — the wrong starting value, or the wrong post-loop computation.
Accumulator trace rows
0 / 5 points
Arrange the trace rows for adding [20, 5, 15] to total.
Tracing a Loop with an if Inside
Combining the two ideas gives you the shape you need for counting and searching loops:
minutes_list = [10, 45, 90, 20, 75]long_sessions = 0
for minutes in minutes_list: if minutes >= 60: long_sessions += 1
print(long_sessions)| Iteration | minutes | if taken? | long_sessions after |
|---|---|---|---|
| (before loop) | — | — | 0 |
| 1 | 10 | no | 0 |
| 2 | 45 | no | 0 |
| 3 | 90 | yes | 1 |
| 4 | 20 | no | 1 |
| 5 | 75 | yes | 2 |
The “if taken?” column is what makes this trace useful. You can see at a glance which iterations actually updated the counter and which did not. If the final answer is wrong, you can scan this column and check whether the right iterations triggered the update.
Debug Prints Inside Loops
Debug prints from Part 1 still work inside loops, but they print on every iteration, which can be loud. Two tactics keep them useful:
Tactic 1: Print only when a condition matches.
minutes_list = [10, 45, 90, 20, 75]long_sessions = 0
for minutes in minutes_list: if minutes >= 60: print(f"DEBUG: counting {minutes} as long") long_sessions += 1This only prints the iterations that triggered the update. You see exactly the cases that mattered, without scrolling through every iteration.
Tactic 2: Print the final state after the loop.
for minutes in minutes_list: if minutes >= 60: long_sessions += 1
print(f"DEBUG: long_sessions = {long_sessions}")This says nothing about what happened inside the loop, but it makes the result obvious before the program does anything else with it.
Both tactics are useful in different situations. The first is good when you suspect the bug is in which iterations triggered an update. The second is good when you suspect the bug is in what happens after the loop.
Add targeted debug prints
0 / 10 points
Start with this list:
minutes_list = [10, 80, 25, 120]Do not ask the user for input in this exercise.
Count how many sessions are at least 60 minutes. Whenever a session is counted, print a debug line in this exact format: DEBUG counting <minutes>.
After the loop, print the final count.
Example input:
(no input)Required output text:
DEBUG counting 80DEBUG counting 120Long sessions: 2A Walked-Through Example
Here is a small program that is supposed to find the longest session in a list. Read it, predict what it prints with minutes_list = [45, 30, 90, 60, 75], and then trace it.
The function call
range(1, len(minutes_list))produces the numbers from1up to but not includinglen(minutes_list).
minutes_list = [45, 30, 90, 60, 75]longest = 0
for i in range(1, len(minutes_list)): if minutes_list[i] > longest: longest = minutes_list[i]
print(f"Longest: {longest}")A trace, one row per iteration:
i | minutes_list[i] | if taken? | longest after |
|---|---|---|---|
| (before) | — | — | 0 |
| 1 | 30 | yes | 30 |
| 2 | 90 | yes | 90 |
| 3 | 60 | no | 90 |
| 4 | 75 | no | 90 |
| (after) | — | — | 90 |
The output is Longest: 90. That happens to be the right answer for this input, but the trace exposes two problems:
-
range(1, len(minutes_list))starts at1, so iteration never seesminutes_list[0], which is45. The first item is silently skipped. With this list, the skip happens not to matter because45is not the largest. But if the largest item were at position0— for example,minutes_list = [90, 30, 45]— the program would give the wrong answer. -
longest = 0only works if every item is positive. If the list were all negative numbers (which is unusual for minutes, but the principle holds),longestwould stay at0and the result would be wrong.
This is the kind of bug that a single test cannot find. The trace is what reveals the gap between “works on this input” and “works on every input”. You will see this pattern again — and learn how to be systematic about it — in the next chapter.
Fix the longest session finder
0 / 10 points
The starter code asks the user for three numbers and stores them in a list. It is supposed to find the largest value in the list, but it has a few bugs.
Fix the program so that it finds the largest value in minutes_list and prints it.
Example input:
453090Required output text:
Longest: 90Check Your Understanding
- Why is it useful to add a “branch taken” column when tracing an
if/elif/else? - In a
forloop trace, why include explicit “before loop” and “after loop” rows? - When debugging a counting loop that gave the wrong answer, which debug-print tactic is more useful: printing every iteration, or printing only the iterations that matched the
if? - Trace the following program by hand, writing the steps on a piece of paper. What does it print?
total = 0for x in [3, 6, 9, 12]: if x % 2 == 0: total += xprint(total)AI Study Aid
Create a chapter diagram
We're looking into to what extent AI-generated diagrams could help with studying.
Use this study aid to generate an AI-generated visual summary of the material you just studied.
Each diagram style emphasizes the content in a different way, so you can choose the focus
that feels most useful.
Using the diagram generator is voluntary and does not affect your course progress. We encourage
you to try it out and see if it helps, but it's totally up to you! Your answers help us understand
how to make better study aids in the future.