Read a Python traceback from the bottom up, recognise common error types and fix bugs methodically.
Why it matters
Every programmer, at every level, writes code that breaks. The difference between a beginner and an experienced developer is not fewer errors, it is how quickly they find the cause. Python is very helpful here: when something goes wrong it prints a traceback that says what failed and exactly where. Learning to read it turns a scary wall of red text into a set of directions.
Anatomy of a traceback
Here is a small script with a bug:
def average(values):
return sum(values) / len(values)
def report(readings):
mean = average(readings)
return f"Mean: {mean:.2f}"
print(report([]))
Running it prints:
Traceback (most recent call last):
File "sensors.py", line 8, in <module>
print(report([]))
File "sensors.py", line 5, in report
mean = average(readings)
File "sensors.py", line 2, in average
return sum(values) / len(values)
ZeroDivisionError: division by zero
Read it from the bottom up:
- The last line names the error type (
ZeroDivisionError) and a message (division by zero). This is what went wrong. - The frame just above it shows where: file
sensors.py, line 2, inside the functionaverage, with the line of code itself. - The frames higher up show how the program got there: line 8 called
report, which on line 5 calledaverage. "Most recent call last" means the deepest call is at the bottom.
So the story is: report([]) passed an empty list to average, and len([]) is 0. The fix might be to handle the empty case in average, or to avoid calling it with no data. The traceback tells you where to look; you still decide what the correct behaviour is.
Common error types
| Error | Usual cause | Example |
|---|---|---|
SyntaxError | Code is not valid Python: missing colon, bracket or quote | if x > 3 |
IndentationError | Inconsistent or missing indentation | a block not indented under if |
NameError | A name is misspelled or used before it is defined | pirnt("hi") |
TypeError | An operation got the wrong type of value | "5" + 3 |
ValueError | Right type, unusable value | int("3.5") |
ZeroDivisionError | Dividing by zero | 10 / 0 |
IndexError | List position does not exist | [1, 2][5] |
KeyError | Dictionary key does not exist | {"a": 1}["b"] |
AttributeError | That type has no such method or attribute | [1, 2].upper() |
A SyntaxError is special: Python finds it before running anything, so no lines run at all. The caret (^) under the code points at where Python got confused, and the real mistake is often just before it, such as an unclosed bracket on the previous line.
Bugs without errors
The hardest bugs raise no error at all: the program runs and gives the wrong answer. An off-by-one loop or a wrong formula looks perfectly fine to Python. For these you need to check results against values you know:
def average(values):
total = 0
for i in range(1, len(values)): # bug: skips the first item
total += values[i]
return total / len(values)
print(average([2, 4, 6])) # 3.3333333333333335, but should be 4.0
Testing with a tiny input whose answer you can work out by hand exposes the bug immediately.
A debugging method
- Reproduce it. Find the smallest input that shows the problem.
- Read the last line, then the nearest frame in your own code. Ignore frames inside libraries at first.
- Look at the values. Add temporary
printcalls to see what variables actually hold:print("DEBUG values =", repr(values)).reprshows quotes around strings, so you can tell"5"from5. - Form one hypothesis and test it. Change one thing, run again. Changing several things at once hides which one mattered.
- Check with
assert.assert average([2, 4, 6]) == 4stops with anAssertionErrorif the result is wrong. The checks in this course's Do tab work exactly this way. - Remove the debug prints when you are done.
Explaining the code line by line to someone else (or to a rubber duck on your desk) is a surprisingly effective step 4. You often spot the bug mid-sentence.
Handling errors you expect
Some errors are not bugs but expected situations, like a sensor sending "n/a" instead of a number. try / except lets you handle them:
def parse_reading(text):
try:
return float(text)
except ValueError:
return None # not a number: skip it
print(parse_reading("21.5")) # 21.5
print(parse_reading("n/a")) # None
Catch the specific error you expect. A bare except: hides every problem, including typos in your own code, and makes debugging much harder.
Common mistakes
- Reading only the top of the traceback. The answer is at the bottom.
- Guessing instead of looking. Print the actual values before changing code.
- Fixing the symptom. Wrapping everything in
try/exceptmakes the error message go away, not the bug. - Assuming input types. Text from files and forms is a string until you convert it.
Quick recap
- Read tracebacks bottom up: error type and message, then the line, then the call chain.
- Learn the common error types; each points to a typical cause.
- Wrong-answer bugs need known test cases and
assert. - Debug one hypothesis at a time, with
print(repr(...))to see real values. - Use
try/except SpecificErroronly for situations you expect.
Try it
The Do tab gives you a small sensor script with three bugs. Run the checks, read each failure message, and fix the code until every check passes.