Debugging and Testing
Errors are evidence, not failures — learn to read a traceback, tell three kinds of bug apart, and turn examples into repeatable checks.
By the end of this lesson, you can
- Distinguish a syntax error, a runtime error, and a logic error, and explain why a logic error is the hardest to notice
- Read a traceback to find which line caused a runtime error
- Turn a worked example into a repeatable check using assert, including at least one edge case
Why it matters
Every program breaks sometimes — that’s normal, not a sign something has gone especially wrong. An error message is evidence: a specific, often precise clue about what happened and where. Learning to read that evidence, rather than treating it as a wall to bang against, is what turns debugging from guesswork into a repeatable process.
Mental model
Python bugs fall into three categories, and each needs a different response:
- Syntax errors — the code doesn’t follow Python’s grammar at all, so nothing runs, not even the parts before the mistake.
- Runtime errors — the code is valid, starts running, but fails partway through a specific line, for a specific input.
- Logic errors — the code runs to completion with no error message at all, but produces the wrong answer.
The first two announce themselves. The third doesn’t — which is exactly why it’s the hardest to catch.
Small example
A runtime error stops the program and prints a traceback:
numbers = [1, 2, 3]
print(numbers[5])Traceback (most recent call last):
File "example.py", line 2, in <module>
print(numbers[5])
IndexError: list index out of rangeRead a traceback from the bottom up: the last line names the error
type (IndexError) and says what happened (list index out of range);
the line above it points at exactly where — line 2, trying to access
index 5 of a list that only has three items (indices 0, 1, 2).
Now compare that to a function with a logic error:
def average(numbers):
total = 0
for number in numbers:
total = total + number
return total
print(average([2, 4, 6]))12No traceback. No error message. It just runs — and quietly returns the
sum, 12, instead of the average, 4, because the function never
divides by how many numbers there were.
Turning an example into a test
An assertion automates exactly that check. Instead of eyeballing
output, assert compares it against an expected value and raises an
error immediately if they don’t match:
def average(numbers):
total = 0
for number in numbers:
total = total + number
return total
assert average([2, 4, 6]) == 4Traceback (most recent call last):
File "example.py", line 6, in <module>
assert average([2, 4, 6]) == 4
AssertionErrorThe bug is now impossible to miss — and unlike reading output by eye, this check runs again automatically every time the code changes. A good set of assertions includes at least one edge case: an input that’s easy to get wrong, like an empty list or a single item.
assert average([2, 4, 6]) == 4
assert average([10]) == 10 # edge case: only one item
Isolating a bug
When a bug isn’t obvious, shrink the program down to the smallest version that still shows it — a minimal reproducible example. Strip away everything that isn’t necessary: unrelated functions, extra data, formatting. A five-line program that still fails is far easier to reason about than the two-hundred-line program it came from, and it’s often obvious what’s wrong once nothing else is left to look at.
Check your understanding
Or reveal the answer without checking
Answer:Line 3, a TypeError
Reading bottom-up: the last line names the error type, TypeError, with a message about mixing int and str. The line above it points at line 3, the print(total + "kg") call, as exactly where it happened.
Or reveal the answer without checking
Answer:A runtime error stops the program and prints a traceback pointing at a line; a logic error produces no error message at all
A runtime error announces itself with a traceback that names a specific line. A logic error runs to completion silently, so there's no automatic pointer to where it happened — only a wrong answer to notice.
Practice: warm-up
Classify each of these as a syntax error, a runtime error, or a logic error:
print("hello"(a missing closing parenthesis)- A function meant to find the largest number in a list, which instead always returns the first number in the list, with no error message
print(10 / 0)
Stuck? Reveal one hint at a time.
Hint 1
Ask first: does the program fail to start at all, fail partway through, or run to completion?
Hint 2
A missing parenthesis breaks Python's grammar before anything runs. Dividing by zero fails while a specific line executes. Returning a plausible-but-wrong answer with no error message is the third category.
Reveal the classification
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
1. print("hello" -> syntax error (unclosed parenthesis; nothing runs)
2. wrong "largest" logic -> logic error (runs fine, wrong answer, no message)
3. print(10 / 0) -> runtime error (ZeroDivisionError, fails on that line)Practice: apply it
Fix the average function from this lesson so it computes an actual
average, not a sum.
Or reveal the answer without checking
Answer:Change return total to return total / len(numbers)
The bug is that the function returns the running sum instead of dividing it by the count of numbers. len(numbers) gives that count.
Modification challenge: after fixing it, write two assert
statements that check it — one ordinary case, and one edge case (a
list with only one number) — so this bug can never come back unnoticed.
Summary
- Syntax errors stop a program before it starts; runtime errors stop it partway through and print a traceback; logic errors don’t stop anything, but produce a wrong answer.
- Read a traceback from the bottom up: the last line names the error, the line above it names where.
- Running without an error is not the same as being correct — only checking output against a known-correct value can catch a logic error.
assertturns a worked example into an automatic, repeatable check; include at least one edge case.- When a bug is unclear, shrink the program to the smallest version that still reproduces it.