Tracing and debugging

Become your own code stepper. Trace a program by hand with a trace table, find an off-by-one bug, add debugging prints, and follow a calm routine for fixing any bug.

Lesson · 15 min

In this lesson you will

  • Trace a short program by hand with a trace table
  • Find and fix an off-by-one error in a loop
  • Use temporary print statements to see what a program is doing
  • Follow a step-by-step routine for debugging

Now that your programs make decisions and go round in loops, they can surprise you. A program can run without a single error message and still do the wrong thing. Finding out why is called debugging, and it’s a skill like any other: there are techniques, and they get easier with practice. The most important one is tracing: following the program yourself, line by line, exactly as Python would.

A bug with no error message

This program is meant to count down 3, 2, 1 and then lift off. Step through it and keep an eye on the output. Something is missing.

Loading the interactive graphic…

This countdown should print three, two, one, then lift off. Let's trace it and see what really happens.

The countdown printed 3 and 2, then lifted off. 1 never appeared. No error, just a wrong answer.

Trace tables

The stepper did the tracing for us. You can do the same thing on paper with a trace table: one column per variable, one column for the condition, one for output, and a new row every time something changes.

Linenn > 1?Output
23
33True
433
52
32True
422
51
31False
61Lift off!

The table points straight at the problem: when n is 1, the question n > 1 is False, so the loop stops one round too early. The fix is to change the condition to n >= 1 (or n > 0). Make that change and check it:

n = 3
while n > 1:
    print(n)
    n = n - 1
print("Lift off!")

This kind of bug, where a loop runs one time too many or one time too few, is so common it has a name: an off-by-one error. Whenever a loop has a < or a >, ask yourself: should it be <= or >=? Check the first and last iterations specifically, because that’s where off-by-one bugs hide. The same goes for range: range(1, 10) stops at 9, not 10.

Debugging prints

Tracing by hand is great for short programs. For longer ones, let Python tell you what’s going on by adding temporary print lines that show the values you’re unsure about. This program should add up the even numbers from 2 to 10 (2 + 4 + 6 + 8 + 10 = 30), but it prints something else:

total = 0
for n in range(2, 10, 2):
    total = total + n
print("Total:", total)

Add a debugging print inside the loop, run it, and look:

total = 0
for n in range(2, 10, 2):
    print("DEBUG n =", n, "total before =", total)
    total = total + n
print("Total:", total)

The debug lines show n going 2, 4, 6, 8 and never reaching 10. It’s range stopping before its stop value again. Change the 10 to 11 (or 12) and the total becomes 30. Then delete the debugging print: it’s scaffolding, not part of the finished program.

A debugging routine

When a program misbehaves, resist the urge to change things at random. Work through this instead:

  1. Reproduce it. Find exact values that cause the problem. “It’s wrong when n is 1” is much more useful than “it’s sometimes wrong”.
  2. Predict. Write down what the program should do with those values.
  3. Trace or print to see what it actually does. Find the first line where reality and your prediction part ways.
  4. Fix one thing, then run again. If you change three things at once, you won’t know which one mattered.
  5. Test the edges: the first and last iteration, zero, empty strings, the biggest values.

A surprisingly good extra trick is rubber duck debugging: explain your code out loud, line by line, to someone, or to a rubber duck on your desk. Saying what each line does often reveals the line that doesn’t do what you thought.

Tracing a decision

Tracing works for if statements too. Without running it, trace this program for hours = 45 and write down what it prints. Then run it to check.

hours = 45
rate = 10
if hours > 40:
    overtime = hours - 40
    pay = 40 * rate + overtime * rate * 1.5
else:
    pay = hours * rate
print("Pay:", pay)

You should get 475.0: 40 normal hours at 10, plus 5 overtime hours at 15. Now trace it for hours = 40. Which branch runs? Is that the right answer? These boundary values, exactly on the edge of a condition, are the ones most worth checking.

Your turn

This exercise has a bug of the off-by-one kind. Trace it, find it, fix it.

Exercise · Python

Fix the off-by-one

This program should print the 7 times table from 1 × 7 up to 10 × 7 and add up all ten answers in total (which should come to 385). It runs without an error, but it’s wrong. Trace it (or add a debugging print), find the off-by-one bug, and fix it with the smallest change you can.

total = 0
for n in range(1, 10):
    answer = n * 7
    print(n, "x 7 =", answer)
    total = total + answer
print("Total:", total)

Check yourself

Self-check · 4 questions

Tracing and debugging

1. What is an off-by-one error?
2. What is a trace table for?
3. n = 3; while n > 1: print(n); n -= 1. Which numbers are printed?
4. When debugging, why change only one thing at a time?