When Things Go Wrong on Purpose
A traceback looks like the program's way of failing at you. Read one from the bottom up instead of the top down, and it turns into the most specific bug report you will ever get for free.
A traceback looks like the program's way of failing at you. Read one from the bottom up instead of the top down, and it turns into the most specific bug report you will ever get for free.
Syntax errors are not exceptions
Every red wall of text Python shows you looks the same at a glance, and treating them as one category costs you time later. A SyntaxError happens before your program runs a single line — Python read the whole file, could not make sense of its grammar, and refused to start at all.
$ >>> def greet(name) File "<stdin>", line 1 def greet(name) ^$ SyntaxError: expected ':'Everything else — KeyError, TypeError, ZeroDivisionError, the ones the picker below shows — is an exception. An exception can only happen after the program has started running, on a line that parsed correctly but turned out, at that moment, to be impossible: a key that was never there, a division by a number that happened to be zero. The distinction matters because only exceptions can be caught with try and except. A SyntaxError in the file you are running has already stopped the program before any except clause of yours gets a chance to run.
Reading a traceback from the bottom, not the top
The last line of a traceback names the exception and the message — that is what actually went wrong. The lines above it, read bottom to top, are the chain of calls that led there: the line your own code was on comes first, then whatever called that, and so on outward.
Here is one with more than one frame in it, which is where the bottom-up habit actually earns its keep.
CATALOGUE = {"bread": 3.2, "eggs": 4.5} def price_for(item): return CATALOGUE[item] def total(cart): return sum(price_for(item) for item in cart) total(["bread", "eggs", "kombucha"])$ Traceback (most recent call last): File "shop.py", line 9, in <module> total(["bread", "eggs", "kombucha"]) File "shop.py", line 6, in total return sum(price_for(item) for item in cart) File "shop.py", line 3, in price_for return CATALOGUE[item] ~~~~~~~~~^^^^^^$ KeyError: 'kombucha'Start at the bottom: KeyError: 'kombucha'. Move up one frame: the crash happened inside price_for, on the line that looks up CATALOGUE[item]. Move up again: price_for was called from total, which was itself called from line 9, at the bottom of the file. Reading top to bottom instead tells you the same thing backwards — you would wade through two frames you do not need yet before reaching the one line that actually matters.
scores = [88, 92, 79]print(scores[3])$ Traceback (most recent call last): File "report.py", line 2, in <module> print(scores[3]) ~~~~~~^^^$ IndexError: list index out of rangeCatching the error you expect, not every error
try and except let a program recover from an error instead of crashing — but only if you catch the specific exception you actually anticipated.
$ >>> try:$ ... price = prices["mango"]$ ... except KeyError:$ ... price = 0$ ...$ >>> price$ 0A single except can also name more than one exception, as a tuple, when the same recovery applies to either: except (KeyError, IndexError): runs the same block whichever of the two happens. Reach for that instead of a bare except the moment you are tempted to write one.
try, except, else, finally: each clause earns its place
Most examples stop at try and except, which makes the other two clauses look optional. They are not decoration — each one runs at a different, precise moment, and reaching for the wrong one is a real source of bugs.
$ >>> try:$ ... price = catalogue[item]$ ... except KeyError:$ ... print(f"No such item: {item}")$ ... else:$ ... print(f"{item} costs {price}")$ ... finally:$ ... print("Checked catalogue.")$ ...- 1
try — the code that might fail
Keep it to the smallest block that can actually raise the exception you are guarding against. Wrapping more than that risks silently catching a bug you never meant to.
- 2
except — runs only if try raised that exception
This is where you recover: log it, substitute a default, or tell the user. It never runs if try succeeded.
- 3
else — runs only if try succeeded
Code here runs when nothing went wrong, and an exception raised inside else is not caught by the except above it. It keeps your success-path code from being accidentally shielded by your own error handling.
- 4
finally — always runs, success or failure
Used for cleanup that has to happen either way, like closing a connection or releasing a lock. It runs even if the except block re-raises the error.
Put the print inside try, right after fetching the price, instead of in else, and a bug in that print statement itself — a typo in the variable name, say — gets reported as though it were a KeyError, when it never was one. else exists specifically to stop that.
Raising one on purpose, before it happens by accident
You can trigger an exception deliberately with raise, which is often clearer than letting bad input travel deep into a program before it fails on its own, somewhere confusing.
$ >>> def set_age(age):$ ... if age < 0:$ ... raise ValueError("age cannot be negative")$ ... return age$ ...$ >>> set_age(-5)$ ValueError: age cannot be negativeFailing loudly and immediately, at the exact line where the impossible value appeared, is far easier to debug than a program that accepts -5 quietly and produces a nonsensical answer four functions later.
The condition you check does not have to be this simple. Validate every assumption the rest of the function depends on at the top, before any of the real work runs — that way a bad call fails on its very first line instead of partway through a calculation that no longer makes sense.
Writing your own exception class
ValueError is honest but generic — catching it also catches every other ValueError anywhere else in the program, for reasons that have nothing to do with the one you meant. A custom exception class costs one line and buys you something to catch precisely.
class InsufficientFundsError(Exception): pass def withdraw(balance, amount): if amount > balance: raise InsufficientFundsError( f"cannot withdraw {amount}, balance is {balance}" ) return balance - amount$ >>> try:$ ... withdraw(50, 75)$ ... except InsufficientFundsError as error:$ ... print(f"Blocked: {error}")$ ...$ Blocked: cannot withdraw 75, balance is 50Inheriting from Exception is enough — pass, with nothing else, is a complete class, because Exception already knows how to store the message you pass it and print it back. Now except InsufficientFundsError: catches exactly this failure, and nothing that merely happens to share a message with it.
Key takeaways
- A SyntaxError happens before your program runs at all — Python could not parse the file. An exception happens afterwards, while the program is executing a line that turned out to be impossible.
- Read a traceback from the bottom: the last line names the actual exception and message, and the frames above it, read upward, are the chain of calls that led there.
- try/except recovers from an error you anticipated. Name the specific exception; a bare except: also swallows bugs you never anticipated and need to see.
- else runs only when the try block succeeded, and finally runs no matter what — even when except re-raises. Neither is decoration; each does something the other cannot.
- raise a specific exception the moment an impossible value appears, and write a custom exception class when the built-in ones do not carry your program's own vocabulary of what went wrong.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.