Skip to main content
Back to module
Python Fundamentals · Loops and Functions

Reading errors and debugging

Reading30mLesson 3 of 3 in this module

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:

  1. The last line names the error type (ZeroDivisionError) and a message (division by zero). This is what went wrong.
  2. The frame just above it shows where: file sensors.py, line 2, inside the function average, with the line of code itself.
  3. The frames higher up show how the program got there: line 8 called report, which on line 5 called average. "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

ErrorUsual causeExample
SyntaxErrorCode is not valid Python: missing colon, bracket or quoteif x > 3
IndentationErrorInconsistent or missing indentationa block not indented under if
NameErrorA name is misspelled or used before it is definedpirnt("hi")
TypeErrorAn operation got the wrong type of value"5" + 3
ValueErrorRight type, unusable valueint("3.5")
ZeroDivisionErrorDividing by zero10 / 0
IndexErrorList position does not exist[1, 2][5]
KeyErrorDictionary key does not exist{"a": 1}["b"]
AttributeErrorThat 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

  1. Reproduce it. Find the smallest input that shows the problem.
  2. Read the last line, then the nearest frame in your own code. Ignore frames inside libraries at first.
  3. Look at the values. Add temporary print calls to see what variables actually hold: print("DEBUG values =", repr(values)). repr shows quotes around strings, so you can tell "5" from 5.
  4. Form one hypothesis and test it. Change one thing, run again. Changing several things at once hides which one mattered.
  5. Check with assert. assert average([2, 4, 6]) == 4 stops with an AssertionError if the result is wrong. The checks in this course's Do tab work exactly this way.
  6. 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 / except makes 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 SpecificError only 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.

Also in this lesson: