Home Tutorials Error Handling

Error Handling

Python Error Messages and Tracebacks: How to Read Them Fast

Pyford Notes July 6, 2026 6 min read
Key points
  • A traceback lists the chain of calls that led to an error, with the actual failure described at the very bottom.
  • Read a traceback from the bottom up: the last line names the exception and the message; the frame just above shows exactly where it happened.
  • The exception's type (ValueError, KeyError, AttributeError, and so on) is usually the fastest clue to what went wrong.
  • "During handling of the above exception, another exception occurred" means one error triggered a second, different error while being handled.

Anatomy of a traceback

When an exception is not caught, Python prints a traceback — a report of the call chain that led to the failure — before the program exits. A typical one looks like this:

Traceback (most recent call last):
  File "app.py", line 12, in <module>
    total = calculate_total(order)
  File "app.py", line 7, in calculate_total
    return sum(item["price"] for item in order)
  File "app.py", line 7, in <genexpr>
    return sum(item["price"] for item in order)
KeyError: 'price'

Each "File ... line ... in ..." block is one frame: the file, line number, and function where execution was at that point in the call chain. The very last line, KeyError: 'price', names the exception type and its message.

Reading bottom to top

The heading says "most recent call last," which means the frame that actually raised the exception is at the bottom, just above the exception line — not the top. The most useful habit when facing an unfamiliar traceback is to read the last two or three lines first:

  • The very last line: the exception type and message — what went wrong.
  • The frame directly above it: the file, line, and code that triggered it — where it went wrong.

In the example above, that means the failure is a missing "price" key, happening inside the generator expression on line 7. The frames above that show how execution got there — calculate_total was called from module-level code on line 12 — which matters for understanding the bug's broader context, but is rarely where the actual fix belongs.

What the exception type tells you

The exception's class name is usually the fastest clue to the shape of the problem, before even reading the message:

Common exception types and what they usually mean
ExceptionUsual cause
KeyErrorA dictionary lookup used a key that does not exist.
IndexErrorA sequence was indexed past its length.
AttributeErrorAn object does not have the attribute or method being accessed — often a None where a real object was expected.
TypeErrorAn operation was applied to a value of the wrong type, or a function was called with the wrong number or kind of arguments.
ValueErrorThe type is correct, but the specific value is not valid for the operation.

An AttributeError: 'NoneType' object has no attribute 'x' is one of the most common in practice, and almost always traces back to a function that returned None unexpectedly, somewhere upstream of the line the traceback points to.

Chained exceptions

Sometimes handling one exception triggers a second, unrelated one. Python reports both, connected by a line explaining the relationship:

Traceback (most recent call last):
  File "app.py", line 4, in <module>
    value = int(raw)
ValueError: invalid literal for int() with base 10: 'abc'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "app.py", line 6, in <module>
    log.write(value)
NameError: name 'value' is not defined

The second traceback is usually the one worth fixing first if it looks unrelated to your actual bug — here, value was never assigned because the first conversion failed, and the real fix is handling the ValueError from parsing raw, not the resulting NameError.

Quick habits for tracking down the cause

A few habits make tracebacks faster to act on:

  • Read the last line first, then the frame directly above it — resist the urge to start at the top.
  • Note the exact line number and open that file at that line before doing anything else.
  • If the traceback runs through library code, look for the last frame that is inside your own project — that is usually where the fix belongs, even if the crash technically happened deeper inside a dependency.
  • For a KeyError or IndexError, print or log the actual dictionary or list just before the failing line to see what was really there instead of guessing.

Tracebacks look intimidating mainly because of their length, not their complexity — every one of them is answering exactly two questions: what failed, and where.

AssertionError and its message

An assert statement checks a condition and raises AssertionError if it is false, optionally with a custom message you supply after a comma:

def process(order):
    assert order["total"] >= 0, f"Negative total: {order['total']}"
    return order["total"]

process({"total": -5})
Traceback (most recent call last):
  File "app.py", line 5, in <module>
    process({"total": -5})
  File "app.py", line 2, in process
    assert order["total"] >= 0, f"Negative total: {order['total']}"
AssertionError: Negative total: -5

The custom message appears right after AssertionError: on the last line, giving you the actual failing value without needing to add a separate print statement first. Because Python's -O optimisation flag strips assert statements entirely, they are best used to catch programmer mistakes and invariants during development, not for validating untrusted external input in a way the program must always enforce.