Master

Debugging strategies

Find and fix bugs systematically instead of guessing at random.

What you will see

[1, 2, 3, 4] [0, 1, 2, 3, 4]

About 2 min of reading

Quick check

Before this new lesson, one small question about the lesson you just finished. Recall it, do not look it up.

Try It

Debugging is a repeatable cycle: reproduce the error, guess the cause, change one thing, then confirm the fix. I add a tracing print, read a traceback's type and message, and compare two ranges to expose an off-by-one bug.

1

I trace a function by printing values as they flow, then read the returned result. A temporary print reveals what the program saw before it finished, which is how you locate a wrong value.

def divide(a, b): print("trace:", a, b) return a / b result = divide(10, 2) print("result:", result)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
trace: 10 2 result: 5.0
The trace print shows exactly what the function received. Remove it once the bug is found.
2

A crash tells you exactly where it got lost. I catch the error and print its type name and message, which names the line class and the problem, so I can fix the right thing.

try: value = 10 / 0 except ZeroDivisionError as e: print(f"Error type: {type(e).__name__}") print(f"Error message: {e}")

Before you run it, say out loud what it will print. Then open the answer.

What it prints
Error type: ZeroDivisionError Error message: division by zero
The message names the error's type and says what happened. A traceback always points at the real problem.
3

Off-by-one bugs come from exclusive ranges. I print two ranges side by side to show that range(1, 5) gives four numbers, not five, which is how the classic error appears.

print(list(range(1, 5))) print(list(range(5)))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
[1, 2, 3, 4] [0, 1, 2, 3, 4]
range(start, stop) includes start but stops before stop. range(1, 5) is 1,2,3,4, one too few if you expected five.

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

A bug is reproduced, the cause is guessed, a single change is tested, and the fix is confirmed.
1 Reproduce the error reliably
↓
2 Read the traceback for the line and type
↓
3 Change one thing and re-run
↓
4 Confirm the fix without breaking others
Line by line

Read the first example one line at a time. A line that prints shows what appears on screen; the rest set things up quietly.

def divide(a, b): sets nothing on screen
print("trace:", a, b) trace: 10 2
return a / b sets nothing on screen
result = divide(10, 2) sets nothing on screen
print("result:", result) result: 5.0
Why this works
The debugging loop

Good debugging follows a repeatable cycle:

  1. **Reproduce*: make the bug happen on purpose.
  2. **Hypothesize*: guess the most likely cause.
  3. **Test*: change one thing and see if it fixes it.
  4. **Confirm*: verify the fix and that nothing else broke.

Reproduce -> Guess -> Change one thing -> Check -> Repeat

Change only one thing at a time, or you won't know what fixed it.

Common bug types

Most beginner bugs are a small set:

  • **Off-by-one*: looping one time too many or too few.
  • **Wrong operator*: = instead of ==.
  • **Uninitialized variable*: using a value before setting it.
  • **Index out of range*: asking for item 5 in a 5-item list.

# off-by-one: range(1, 5) gives 1,2,3,4 (not 5)
for i in range(1, 5):
    print(i)

Spotting which category a bug belongs to makes the fix fast.

Examples

Read the error message

Errors tell you what went wrong

File "app.py", line 5
ZeroDivisionError: division by zero

The message names the type of error and the line. That is half the fix. Never ignore the traceback, it points straight at the problem.

Why it works: a crash is the computer telling you exactly where it got lost.

Print statements and tracing

Checking values as they flow

def divide(a, b):
    print("debug:", a, b)   # temporary
    return a / b

divide(10, 0)

Add a print to see the values at each step, then remove it once the bug is found. This is called tracing.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Loops & iteration
Previous Lesson
Loops & iteration