Kiru Lab  /  Foundations: Python as an Instrument  /  Getting Set Up

Reading Error Messages

You will see thousands of these. Learning to read one properly is worth more than any syntax you memorize.

Hands-On Lab  ·  about 40 minutes

Beginners see an error and feel that something has gone wrong with them. Experienced engineers see an error and feel relief, because the alternative — code that runs and quietly produces the wrong answer — is far worse. An error is the interpreter telling you exactly where it stopped and why. Learn to read it and most problems solve themselves.

Anatomy of a traceback

A real traceback, annotated
Traceback (most recent call last):
  File "analysis.py", line 12, in <module>
    result = average(scores)          <- the line that called the failing code
  File "analysis.py", line 7, in average
    return total / len(values)        <- the line that actually failed
ZeroDivisionError: division by zero    <- what went wrong
  1. Read the bottom line first. It names the error type and gives a one-line description. That is usually enough.
  2. Read the second-to-last file/line pair. That is where the failure happened.
  3. Read upward only if you need to know how you got there. The frames above are the chain of calls that led to the failure.
  4. Ignore frames pointing into library code you did not write, until you have ruled out your own.

The five errors you will meet first

  • SyntaxError — Python could not even parse the file. Usually a missing colon, bracket, or quote, and often on the line *before* the one reported.
  • IndentationError — your spacing is inconsistent. Pick four spaces and never mix tabs with spaces.
  • NameError — you used a name that does not exist. Usually a typo, or you used it before assigning it.
  • TypeError — you did something to a value that its type does not support, like adding a number to a string.
  • IndexError / KeyError — you asked for position or key that is not there. Remember lists start at index 0.
Generate each one deliberately
print("unclosed          # SyntaxError

def f():
return 1                  # IndentationError

print(undefined_name)     # NameError

print("age: " + 30)       # TypeError: can only concatenate str to str

print([1, 2, 3][5])       # IndexError: list index out of range

When there is no error but the answer is wrong

This is the harder case, and the tool is the same one professionals use: print the values. Put a `print()` on either side of the line you suspect and look at what is actually there rather than what you assume is there. The gap between those two is where every bug lives.

Debugging by printing
def average(values):
    print("got:", values, "type:", type(values))   # what did I actually receive?
    total = sum(values)
    print("total:", total, "count:", len(values))  # are both what I expect?
    return total / len(values)
This habit scales all the way up

In track seven you will debug a language model by hooking its internals and printing activation shapes. It is the same move you are learning right now: stop assuming, look at the actual value. The scale changes; the instinct does not.


Hold on to

  • An error is information, not judgment — read the bottom line first
  • Five error types cover almost everything you will hit early
  • When there is no error, print the values and compare against your assumption

Work through

Try each one before opening the solution. Getting it wrong first is most of where the learning happens.

  1. Deliberately produce all five error types from the lesson. For each, write down the bottom line of the traceback and the fix in your own words.
    Hint

    Copy each snippet from the lesson into its own file and run it one at a time.

    Solution

    SyntaxError: unterminated string — add the closing quote. IndentationError: expected an indented block — indent the body four spaces. NameError: name is not defined — assign it first, or fix the typo. TypeError: can only concatenate str — convert with `str(30)` or use an f-string. IndexError: list index out of range — the list has 3 items, so valid indices are 0, 1, 2.

    print("closed now")             # SyntaxError fixed
    
    def f():
        return 1                    # IndentationError fixed
    
    defined_name = 1
    print(defined_name)             # NameError fixed
    
    print("age: " + str(30))        # TypeError fixed
    print(f"age: {30}")             # or better
    
    print([1, 2, 3][2])             # IndexError fixed
  2. Write a function that divides two numbers, call it with a zero denominator, and read the traceback. Identify which line failed and which line called it.
    Hint

    The traceback lists frames oldest first — the failing line is the last file/line pair.

    Solution

    The bottom frame points at `return a / b`, which is where the division happened. The frame above it points at your call site. The bottom line names the type: ZeroDivisionError. Fixing it means deciding what division by zero should mean for your program — raise a clearer error, or return a defined value — which is a design choice, not a syntax fix.

    def divide(a, b):
        if b == 0:
            raise ValueError("denominator must be non-zero")
        return a / b
    
    divide(1, 0)   # now fails with a message that says what you did wrong
    Check your work

    Paste this after your own code. If it runs without raising, you have it.

    try:
        divide(1, 0)
    except ValueError as exc:
        assert "non-zero" in str(exc)
    else:
        raise AssertionError("expected a ValueError")
    assert divide(6, 3) == 2
    print("ok")
  3. Take a function that returns a wrong answer (write one on purpose — say, a sum that forgets the last element), and find the bug using only print statements. Note how many prints it took.
    Hint

    Print the inputs at the top of the function and the result just before returning.

    Solution

    The classic off-by-one: `range(len(values) - 1)` stops one element early. Two prints find it — one showing the input has 5 items, one showing the loop ran 4 times. Most bugs need two or three prints, not a debugger. The number of prints it took you is a useful thing to notice, because it drops fast with practice.

    def broken_sum(values):
        total = 0
        for i in range(len(values) - 1):   # bug: misses the last element
            total += values[i]
        return total
    
    def fixed_sum(values):
        total = 0
        for value in values:               # iterate the items, not the indices
            total += value
        return total
    Check your work

    Paste this after your own code. If it runs without raising, you have it.

    assert fixed_sum([1, 2, 3, 4, 5]) == 15
    assert fixed_sum([]) == 0
    print("ok")

Sign in to track your progress through the lab.