Programming Essentials~25 min

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:

  1. Syntax errors — the code doesn’t follow Python’s grammar at all, so nothing runs, not even the parts before the mistake.
  2. Runtime errors — the code is valid, starts running, but fails partway through a specific line, for a specific input.
  3. 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:

Python
numbers = [1, 2, 3]
print(numbers[5])
Output
Traceback (most recent call last):
  File "example.py", line 2, in <module>
    print(numbers[5])
IndexError: list index out of range

Read 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:

Python
def average(numbers):
    total = 0
    for number in numbers:
        total = total + number
    return total

print(average([2, 4, 6]))
Output
12

No 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:

Python
def average(numbers):
    total = 0
    for number in numbers:
        total = total + number
    return total

assert average([2, 4, 6]) == 4
Output
Traceback (most recent call last):
  File "example.py", line 6, in <module>
    assert average([2, 4, 6]) == 4
AssertionError

The 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

Given this traceback, what line caused the error, and what type is it? Traceback (most recent call last): File "example.py", line 3, in <module> print(total + "kg") TypeError: unsupported operand type(s) for +: 'int' and 'str'
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.

Why is a logic error usually harder to find than a runtime error?
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:

  1. print("hello" (a missing closing parenthesis)
  2. 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
  3. print(10 / 0)

Stuck? Reveal one hint at a time.

  1. Hint 1

    Ask first: does the program fail to start at all, fail partway through, or run to completion?

  2. 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.

Which single change fixes it?
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.
  • assert turns 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.