Your script crashes, a wall of red text fills the terminal, and the instinct is to scroll straight to the top and start reading. That’s the wrong direction — and it’s probably why tracebacks feel scarier than they are.
A traceback isn’t Python telling you that you’ve broken something beyond repair. It’s Python telling you exactly where execution stopped and why, in more detail than almost any other language bothers to give you. The trick is reading it in the right order.
Read it bottom to top
Take this traceback:
Traceback (most recent call last):
File "main.py", line 8, in <module>
result = divide(10, 0)
File "main.py", line 4, in divide
return a / b
ZeroDivisionError: division by zero
Start at the last line — that’s the actual error: ZeroDivisionError: division by zero. That alone tells you what kind of problem you’re dealing with.
The line right above it shows exactly where inside the code that error was raised — here, the return a / b line inside divide().
Working further upward traces the chain of calls that got you there, oldest call at the top. For a short traceback like this one, you don’t need much more than the bottom two lines: the exception type and message, and the exact line that raised it. Everything above that is just context for how execution arrived there — useful when the bug isn’t obvious, skippable when it is.
The most common way people make it worse
There’s a real temptation, especially under a deadline, to wrap the crashing line in a bare except: and move on:
try:
risky_operation()
except: # catches everything, including real bugs
pass
Don’t. A bare except catches everything — typos, keyboard interrupts, bugs you don’t know exist yet — and silently hides all of them. Catch the specific exception you’re actually expecting instead:
try:
risky_operation()
except ValueError:
handle_bad_input()
Now anything you didn’t anticipate still surfaces as a real, readable traceback instead of vanishing.
Two mistakes worth knowing about ahead of time
Assuming the error is where the traceback “feels” like it should be. Often the actual bug happened several lines earlier — a variable got set incorrectly, and the crash is just where that bad value finally caused a visible problem. The traceback tells you where things stopped, not necessarily where they went wrong.
Fixing the symptom instead of the cause. Wrapping a crashing line in try/except and moving on makes the error message go away, but the bad state that caused it is usually still there — it just surfaces somewhere else, later, and harder to trace back.
A debugging habit that actually works
Read the full error message and the exact line it points to before changing anything. Reproduce the bug with the smallest input that still triggers it. Add a print() — or drop a breakpoint() right before things go wrong — and check your assumptions about a variable’s type and value directly with type(x) and print(x) rather than guessing. Change one thing at a time; resist fixing five suspected causes simultaneously, because when it works you won’t know which one mattered.
And when you’re genuinely stuck: explain the problem out loud, line by line, as if to someone else. Naming your assumptions explicitly is often enough to spot the one that’s wrong.