Making Decisions and Repeating Work

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 for or while loop 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)
StepLineBranchState afterOutput
1minutes = 45—minutes = 45(nothing)
2if minutes < 15:not takenminutes = 45(nothing)
3elif minutes <= 90:takenminutes = 45, response = "Solid."(nothing)
4print(response)—unchangedSolid.

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)
Iterationminutestotal afterOutput
(before loop)—0(nothing)
14545(nothing)
23075(nothing)
360135(nothing)
(after loop)—135135

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)
Iterationminutesif taken?long_sessions after
(before loop)——0
110no0
245no0
390yes1
420no1
575yes2

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 += 1

This 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 80
DEBUG counting 120
Long sessions: 2

A 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 from 1 up to but not including len(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:

iminutes_list[i]if taken?longest after
(before)——0
130yes30
290yes90
360no90
475no90
(after)——90

The output is Longest: 90. That happens to be the right answer for this input, but the trace exposes two problems:

  1. range(1, len(minutes_list)) starts at 1, so iteration never sees minutes_list[0], which is 45. The first item is silently skipped. With this list, the skip happens not to matter because 45 is not the largest. But if the largest item were at position 0 — for example, minutes_list = [90, 30, 45] — the program would give the wrong answer.

  2. longest = 0 only works if every item is positive. If the list were all negative numbers (which is unusual for minutes, but the principle holds), longest would stay at 0 and 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:

45
30
90

Required output text:

Longest: 90

Check Your Understanding

  1. Why is it useful to add a “branch taken” column when tracing an if/elif/else?
  2. In a for loop trace, why include explicit “before loop” and “after loop” rows?
  3. 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?
  4. Trace the following program by hand, writing the steps on a piece of paper. What does it print?
total = 0
for x in [3, 6, 9, 12]:
if x % 2 == 0:
total += x
print(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.

Diagrams tried: 0