2.6 Errors, exceptions and defensive code
Checked against the Python 3 documentation, August 2026
What this is and why it exists
There are two kinds of failing program: one that tells you what went wrong and what to do about it, and one that presents a riddle. The difference is not luck — it is a handful of decisions about what to catch, what to raise and what to record. This topic is those decisions, and it contains the single worst habit in beginner Python, which is catching everything and saying nothing.
The vocabulary
- Exception — an object describing something that went wrong, which travels up until something handles it.
- Raise — to produce an exception deliberately.
- Catch (handle) — to intercept one and decide what happens next.
- Traceback — the printed chain of calls showing where the exception came from.
- Custom exception — your own exception class, so callers can catch your failure specifically.
- Assertion — a check of something you believe is always true, which can be turned off in optimised runs.
- Validation — a check of something that might genuinely be wrong, which must never be turned off.
- Logging — recording what happened, with a level and a timestamp, instead of printing.
The mental model
The four clauses do different jobs and the documentation is exact about the last two. The try block holds the risky thing. An except block handles one kind of failure. The else clause "is useful for code that must be executed if the try clause does not raise an exception", and the reference gives the reason to use it: "it avoids accidentally catching an exception that wasn't raised by the code being protected". The finally clause is "intended to define clean-up actions that must be executed under all circumstances" and "runs whether or not the try statement produces an exception".
Which gives the shape to aim for: put as little as possible inside try, put the follow-on work in else, put the cleanup in finally — and better still, let a with block do the cleanup, since that is what context managers are for.
Then the rule this topic exists to teach: catch what you can do something about. An exception you cannot handle should travel upward, because the traceback it produces is more useful than any message you would have invented. A bare except: is the opposite of that — it swallows the typo that would have been obvious, the interrupt you pressed to stop the program, and the memory error that means something serious. If you must catch broadly, catch Exception rather than everything, log it with the traceback, and re-raise unless you genuinely recovered.
Raising your own exceptions is the other half. The documentation says exceptions "should typically be derived from the Exception class" and that "most exceptions are defined with names that end in Error". Defining one costs two lines and buys a real thing: callers can catch your specific failure without catching everything else, and the name itself documents what went wrong.
Assertions and validation look alike and are not. An assertion states something you believe is always true — an internal invariant, a sanity check on your own logic — and it can be turned off when Python runs optimised, so it must never be the thing standing between bad input and your database. Validation is for data that might genuinely be wrong: user input, a file's contents, an interface's response. Validate at the boundary, where data enters your program, and assert inside, where you are checking yourself.
Finally, logging instead of printing. A print goes to standard output with no timestamp, no severity and no way to turn it down. A log line carries a level, so the same code can be quiet in production and detailed while you debug; it carries the time, so you can line it up against something else that happened; and it can be sent somewhere other than the terminal. The rule of thumb: print is for a program's output, logging is for its narration.
In code
Checked against the errors tutorial and the logging reference.
import logging
logger = logging.getLogger(__name__)
class ScoreFormatError(Exception):
"""A row's score could not be read as a number."""
def parse_score(raw, row_number):
try:
value = float(raw)
except ValueError as exc:
raise ScoreFormatError(f"row {row_number}: {raw!r} is not a number") from exc
else:
if not 0 <= value <= 100:
raise ScoreFormatError(f"row {row_number}: {value} is outside 0-100")
return value
def parse_all(rows):
good, bad = [], 0
for i, raw in enumerate(rows, start=1):
try:
good.append(parse_score(raw, i))
except ScoreFormatError:
logger.warning("skipping row %d", i, exc_info=True)
bad += 1
logger.info("parsed %d rows, skipped %d", len(good), bad)
return goodThree things are deliberate here. The from exc keeps the original error attached, so the traceback shows both what you raised and what caused it. The range check sits in else, so it cannot accidentally catch a ValueError from itself. And the caller catches ScoreFormatError specifically — a bad row is skipped and counted, while any other exception travels on to whoever can actually deal with it.
What you should now be able to explain or do
Say what each of the four clauses is for, and why else is better than putting the same code in try. State the rule for what to catch, and name three things a bare except: swallows. Define a custom exception and say what a caller gains from it. Distinguish an assertion from validation and place each correctly. Replace prints with logging and say what three things you gained.
Check yourself
What is wrong with a bare except:?
It catches everything — including your typo, the interrupt you pressed to stop the program, and errors that mean something serious. Catch what you can act on, and let the rest travel up with its traceback intact.
Why put the follow-on work in else rather than at the end of try?
Because anything inside try is protected by the except clauses, so an unrelated error raised by your follow-on code would be caught by a handler written for something else. The else clause runs only when nothing was raised.
What does a custom exception buy a caller?
The ability to catch your failure specifically, without catching everything else — and a name that says what went wrong without reading your code.
Where do assertions belong, and where must they never be?
Inside, checking your own logic. Never between untrusted input and anything that matters: assertions can be turned off in optimised runs, and validation must not be optional.
Name three things logging gives you that print does not.
A severity level so output can be turned down, a timestamp so events can be lined up, and a destination that is not necessarily the terminal.
Go deeper
We haven't checked most of these for screen reader use yet.
- Week 3 Exceptions · Harvard CS50 · Videovideo, with transcript
Back to Errors, exceptions and defensive code: work through the checklist