Modules/Python/ When It Breaks

Reading Errors and Tracebacks

An error is not the program scolding you. It is Python telling you exactly what it could not do, and on which line - once you know how to read it.

Overview

The problem it solves

When Python cannot do what a line asks, it stops and prints a traceback. Read it from the bottom up: the last line is the error type and a plain-English description, and the line just above tells you which line of your code triggered it. Everything higher is the path that led there, which matters only once you have functions calling functions.

Read the last line first

The bottom line names the problem. The line above it points at your code. Run this and read it that way.

Python 3
Output

                            

One at a time

Four common errors, three of them commented out. Uncomment one, run, read the message, then move to the next.

Python 3
Output

                            

The errors you will meet

NameError - you used a name Python has never seen. Usually a typo, or a variable used before it was assigned.
TypeError - the types do not fit the operation, like adding a string to a number. Fix by converting: str(36) or int("36").
ValueError - the type is right but the value is impossible. int("twelve") is a string, as required, but not one that is a number.
IndexError / KeyError - you asked a list or dictionary for something it does not have.

Reading Errors and Tracebacks: A Practical Guide

The message is the instruction.

A traceback is a map, read from the bottom

An error message looks intimidating and is actually the most helpful output Python produces. It has three parts, and the useful one is at the bottom.

Traceback (most recent call last):
  File "app.py", line 42, in <module>
    total = calculate(order)
  File "app.py", line 18, in calculate
    return sum(item.price for item in order.items)
AttributeError: 'NoneType' object has no attribute 'items'

Read it like this:

  1. The last line is what went wrong: an AttributeError, and the description says something was None when an object was expected.
  2. The line above it is where it went wrong: line 18, inside calculate.
  3. The lines above that are how the program got there: line 42 called calculate.

"Most recent call last" is the key phrase in the header. The bottom frame is where the exception was raised; the frames above are the trail of calls that led to it.

When a traceback is long and full of library code, look for the last line that mentions your file. That is almost always where the problem actually lives — the library is just the messenger.

The error happens after the mistake

This is the idea that turns debugging from guesswork into a method: the line that fails is often not the line that is wrong.

In the example above, line 18 is perfectly reasonable code. The real mistake is wherever order was set to None — possibly a function that returned nothing because a lookup failed, twenty lines earlier.

So the question to ask is never only "what is wrong with this line?" but "how did this value get here?" Two tools answer it immediately:

print(f"{order=}")        # shows both the name and the value

and, when printing is not enough:

breakpoint()              # drops into the debugger at this line

At the debugger prompt, p order prints a value, l lists the surrounding code, u moves up a frame to the caller, and c continues. Moving up the frames is the part people miss, and it is exactly how you find where a bad value originated.

The errors you will meet most

ErrorMeansUsual cause
SyntaxErrorPython could not parse the fileMissing bracket, quote or colon
IndentationErrorIndentation is inconsistentMixed tabs and spaces
NameErrorName does not existTypo, or used before defined
TypeErrorWrong type for the operationText where a number was expected
ValueErrorRight type, impossible valueint("abc")
AttributeErrorNo such attribute or methodObject is None, or the wrong type
KeyErrorKey not in the dictionaryMissing field in a response
IndexErrorIndex out of rangeOff-by-one, or an empty list
ZeroDivisionErrorDivision by zeroAn empty denominator
ImportErrorModule not foundNot installed, or the wrong environment

Two of those have a signature cause worth memorising. AttributeError: 'NoneType' object has no attribute ... almost always means a function returned None when you expected a value — frequently a method like sort() that changes something in place. And SyntaxError reported on a line that looks fine usually means an unclosed bracket on the line above.

Things to try

  1. Run the first editor. Note that about to fail prints first: everything before the error ran normally, and execution stopped at the failing line.
  2. Read it bottom-up. The last line says IndexError: list index out of range; the line above names line 3. Three items, so the highest valid index is 2.
  3. Run the second editor as-is. A NameError for totl - a typo of total, which is what that error almost always means.
  4. Work through the rest. Comment out the NameError line, uncomment the TypeError one, and run. Repeat for ValueError and ZeroDivisionError, reading each message before fixing it.

Handling errors on purpose

Once you can read an error, the next step is deciding which ones your program should survive.

try:
    age = int(input("Age: "))
except ValueError:
    print("That is not a whole number.")
else:
    print(f"Next year you will be {age + 1}")
finally:
    print("Done")
  • try holds the risky code, and as little else as possible.
  • except catches one specific exception type. Catch the narrowest type that fits.
  • else runs only if nothing was raised.
  • finally runs either way — for cleanup.

The rule that separates useful error handling from harmful error handling:

try:
    risky()
except Exception:
    pass                  # never do this

A bare except with pass hides the message that would have told you what went wrong, and the program carries on in a state you no longer understand. If you genuinely cannot handle an error, let it propagate — a crash with a traceback is far more useful than silent corruption.

When you do catch something, either fix the situation or re-raise with context:

try:
    config = load(path)
except FileNotFoundError as exc:
    raise RuntimeError(f"config missing at {path}") from exc

from exc keeps the original traceback attached, so you get both the what and the why.

A debugging routine that works

  1. Read the last line of the traceback. Name the exception type and read the message properly — it is usually specific.
  2. Find the last frame in your own code. That is where to start looking.
  3. Print the values involved, with f"{value=}" so you see the name, the value and the quoting.
  4. Check the type, not just the value. print(type(x)) resolves a surprising share of TypeErrors.
  5. Work backwards to where the bad value came from.
  6. Reduce the case. Cut the input down until the smallest thing that still fails; the answer is often obvious by then.
  7. Change one thing at a time, and re-run.

Two extras worth having: logging instead of print once a program is more than a script, and python3 -m pdb script.py to start under the debugger from the first line.

The messages worth recognising on sight

Some error messages have one overwhelmingly likely cause, and knowing them turns a search into a glance.

**'NoneType' object has no attribute ...** — a function returned None when you expected a value. Very often an in-place method: items = items.sort() or text = text.replace(...) without keeping the result.

**'NoneType' object is not subscriptable** — the same cause, one line later: something returned None and you indexed it.

**list indices must be integers or slices, not str** — you have a list where you thought you had a dictionary. Usually the JSON you parsed is an array at the top level, not an object.

**unhashable type: 'list'** — a list used as a dictionary key or put in a set. A tuple is the fix.

**can only concatenate str (not "int") to str** — + between text and a number. An f-string is the fix, not str() around everything.

**SyntaxError on a line that looks perfect** — a bracket or quote left open on the line above. Python only notices when it reaches the next statement.

**IndentationError: unexpected indent** — usually tabs mixed with spaces, which look identical on screen.

**local variable referenced before assignment** — the name is assigned somewhere later in the same function, which makes it local everywhere in that function, including above the assignment.

Each of these is worth reading as a sentence about *what Python found*, not about what you meant. The message describes the state of the program at the moment it stopped, and that state is a fact you can work backwards from.

Errors inside a loop

An exception raised inside a loop ends the whole loop, not just that pass, and that is worth deciding about deliberately rather than discovering.

For a batch job over many records, one malformed row should usually not stop the other nine thousand. The shape that handles it wraps the smallest failing step, records what went wrong with enough detail to find the row again, and continues:

for n, row in enumerate(rows, start=1):
    try:
        process(row)
    except ValueError as exc:
        print(f"row {n}: skipped - {exc}")

Two details make this useful rather than merely quiet. The row number comes from enumerate, so the report says *which* row, which is the difference between a message you can act on and one you cannot. And the exception's own text is included rather than replaced by a generic phrase, because that text is the specific reason.

The opposite decision is equally legitimate. If one bad row means the input file is wrong, stopping immediately is correct, and catching the error would turn a clear failure into a partial output nobody notices is partial. What is not legitimate is except Exception: pass, which chooses neither and discards the evidence for both.

A middle course worth knowing: collect the failures rather than printing them, and raise at the end if there were any. That processes everything it can, reports every problem in one go rather than one per run, and still fails overall — which is usually what a person running a batch actually wants.

Errors you raise yourself

Reading errors well is half of it; the other half is producing ones the next person can read.

Name the value. raise ValueError(f"age must be positive, got {age!r}") ends a debugging session that raise ValueError("invalid input") would have started. The !r matters — it shows quotes and whitespace, so a stray space in a string is visible rather than invisible.

Pick the right type. ValueError when the value is wrong, TypeError when the type is wrong, KeyError or IndexError for lookups, FileNotFoundError and its OSError relatives for the filesystem. A caller can then catch exactly what they can handle, which they cannot do if everything is a bare Exception.

Raise early. Validating an argument at the top of a function reports the problem at the call that caused it. Letting a bad value travel three functions deep produces a traceback pointing at code that is entirely correct.

Say what should happen instead, where it is not obvious. "config missing at /etc/app.conf" is better than "config missing", and "expected one of: red, green, blue" is better than "invalid colour".

The test is whether the message alone, without the traceback, would tell somebody what to fix. That is a high bar and worth aiming at, because the person reading it is usually you, six months later, with no memory of the function at all.

Questions people ask

Why does the error point at a line that looks correct? Because a bracket or quote opened on a previous line was never closed, so Python only noticed at the next statement.

What is the difference between an error and an exception? In Python they are the same thing — exceptions are objects, and errors are the ones that reach the top and stop the program.

Should I catch Exception? Only at the very edge of an application, and only to log properly before exiting. Never to ignore.

What is raise on its own? Inside an except block, it re-raises the exception currently being handled, preserving the original traceback.

How do I see errors in a notebook? The traceback appears under the cell, and %debug in the next cell opens a post-mortem debugger at the point of failure.

Can I make my own exceptions? Yes, and you should for libraries: class ConfigError(Exception): pass lets callers catch exactly your failure and nothing else.

Why does my traceback show library code I never wrote? Because the exception was raised inside the library, called from your code. Find the last frame naming your own file — that is where to start.

What is the difference between an error and a warning? A warning is printed and execution continues; an error stops the program unless something catches it.

Can I make tracebacks shorter? traceback.print_exc(limit=n) trims frames, and some test runners do it for you. The bottom frames are the ones you usually want, so trim from the top.

Recap in one screen

  • Read a traceback from the bottom: the last line says what, the frames above say where and how.
  • The failing line is often not the mistaken line — trace the bad value back to its source.
  • f"{value=}" and breakpoint() answer most "how did it get like this" questions.
  • Catch specific exceptions, keep try blocks small, and never swallow an error with pass.
  • Reduce the failing case until it is small; the cause is usually visible by then.

Check yourself

0 of 3

Answer without scrolling back up.

  1. Which line of a traceback names the actual problem?

  2. print(totl) when you meant total raises:

  3. int("twelve") raises ValueError rather than TypeError because:

Cheat sheet

Reading Errors and Tracebacks

An error is not the program scolding you. It is Python telling you exactly what it could not do, and on which line - once you know how to read it.

PYTHON · vizlearn.in/python/reading_errors.html

Further reading

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.