How to Read Python Stack Traces (Without Losing Your Mind)
A practical, no-fluff guide to decoding Python tracebacks — what each line means, how to jump straight to the real bug, and the five errors that cause 80% of Python pain.
Python tracebacks look intimidating until you realize they’re just a stack of breadcrumbs. The interpreter is telling you exactly where it broke, in the exact order the calls happened. The trick is reading them from the right end.
Read tracebacks bottom-up, then top-down
The last line of a traceback is almost always the one you want:
Traceback (most recent call last):
File "app.py", line 42, in <module>
main()
File "app.py", line 31, in main
user = load_user(user_id)
File "app.py", line 18, in load_user
return db["users"][user_id]
KeyError: 42
The actual error is KeyError: 42 — the key 42 didn’t exist in the users dict. Everything above it is the call chain that got you there. Start at the bottom to see what went wrong, then walk up to see why it happened.
The five Python errors you’ll hit most
1. KeyError / IndexError
You asked a dict for a key it doesn’t have, or a list for an index past its length.
user = {"name": "Alice"}
print(user["age"]) # KeyError: 'age'
Fix: use .get() with a default, or check membership first.
print(user.get("age", "unknown"))
2. AttributeError: 'NoneType' object has no attribute 'x'
You called a method on something that turned out to be None. Usually because an upstream function returned None implicitly (no return statement) or a query came back empty.
Fix: trace back to whatever produced the None. A quick print(repr(x)) one line before the crash is faster than a debugger for this.
3. TypeError: expected X, got Y
You passed the wrong type — often a string where an int was expected (common with input() which always returns strings).
age = input("age: ")
if age > 18: # TypeError: '>' not supported between 'str' and 'int'
Fix: cast explicitly: age = int(input("age: ")).
4. ImportError / ModuleNotFoundError
Either the package isn’t installed, or Python can’t find it in your PYTHONPATH. The second case usually means you’re in the wrong virtualenv.
Fix: which python and pip list — confirm you’re in the env you think you’re in. In Docker or CI, print sys.path at the top of the entrypoint.
5. IndentationError / SyntaxError
You mixed tabs and spaces, or forgot a colon after def/if/for. Modern editors catch these, but they still sneak in through copy-paste.
Fix: run a linter (ruff, flake8) as part of your editor setup. These errors stop being a thing once the linter lives in your save hook.
The lines in the middle matter too
When the bottom line says something generic like TypeError: unsupported operand, the middle of the traceback tells you which line of your code triggered it. Skip over frames that live inside library code — they’re usually not where the bug is. Look for the deepest frame that’s still in your own files.
Async tracebacks: the nasty case
Tracebacks inside asyncio code sometimes show up as a wall of asyncio/base_events.py frames with no sign of your code. When this happens:
- Look for the
During handling of the above exception, another exception occurred:divider — the original exception is often hidden behind a wrapper. - Use
asyncio.run(main(), debug=True)— debug mode surfaces the real frames. - Add
logging.basicConfig(level=logging.DEBUG)at startup to see the actual task chain.
Chained exceptions
Python 3 shows you the full causal chain:
TypeError: ...
The above exception was the direct cause of the following exception:
ValueError: ...
The first exception (the TypeError above) is the root cause. The second is what got raised in the handler. If you want to preserve the link when re-raising, use raise NewError(...) from original. If you want to hide it, use from None.
When you’re truly stuck
Some tracebacks are unreadable because the library decided to wrap everything. SQLAlchemy, Django ORM, and Celery are all repeat offenders. When a stack is 60 frames deep and all the interesting ones are in third-party code, two things help:
- Search the exact error message (with any variables replaced by
...) — there’s usually a GitHub issue or Stack Overflow thread. - Paste the whole thing into an explainer tool that can match the pattern against common causes.
That second one is literally what we built — tuput’s Error Explainer takes any stack trace (Python, JS, Go, Rust, Java) and returns the likely cause in plain English, plus a fix. Free, no sign-up, 5 runs per day.
The meta-tip
Reading tracebacks is a skill that compounds. The first hundred are painful. After that, your eye goes straight to the useful line and you stop seeing the noise. Every traceback you decode the slow way is deposit in a bank you’ll withdraw from for the rest of your career.
Don’t skip the reading. But also — when you’ve spent ten minutes on one and it’s still opaque, paste it somewhere that can match it against a library of known patterns. There’s no medal for suffering through it alone.