Errors & Exceptions
Read error messages, catch exactly the errors you expect, raise your own, chain them, clean up reliably, and handle several errors at once.
You will learn
- Syntax errors vs exceptions, and how to read a traceback.
try/except/else/finally, and the order they run in.- Raising exceptions, chaining with
raise โฆ from, and writing custom exception classes. - Clean-up with
finallyandwith. ExceptionGroup,except*, andadd_note().
Syntax errors
A syntax error means Python couldn't even parse your code, so nothing runs. The parser points with ^ at the place where it got confused. The real mistake is often just before that point.
Here the colon after True is missing.
Exceptions
Code that is syntactically fine can still fail while running. Those run-time errors are exceptions. If nothing handles one, the program stops and prints a traceback. The last line gives the exception type and a message.
Python's error messages keep getting friendlier. Python 3.15 even suggests fixes for common mix-ups from other languages:
The common built-in exceptions
| Exception | Typical cause |
|---|---|
ValueError | right type, wrong value: int("abc") |
TypeError | wrong type: "a" + 1, or calling with the wrong number of arguments |
NameError | a name that was never defined (often a typo) |
AttributeError | no such attribute or method: [].push |
IndexError / KeyError | list index out of range / dict key missing (both are LookupError) |
ZeroDivisionError | dividing by zero (an ArithmeticError) |
FileNotFoundError, PermissionError | file problems (both are OSError) |
KeyboardInterrupt | the user pressed Ctrl+C (not a subclass of Exception) |
Handling exceptions: try / except
Put the risky code in try. If an exception happens, Python skips the rest of the try block and looks for an except clause that matches the exception's type.
The rules:
- The
tryblock runs. - If no exception happens, all the
exceptclauses are skipped. - If an exception happens, the rest of
tryis skipped, and the first matchingexceptruns. - If no
exceptmatches, the exception travels up to the caller. If nobody handles it, the program stops with a traceback.
Several exception types
๐ New in 3.14 (PEP 758): when you don't use as, the parentheses are optional: except ValueError, TypeError:. Older code always writes except (ValueError, TypeError):, and that form is still required with as name.
as err binds the exception object. str(err) is its message, and err.args holds the arguments it was created with.
Order matters: specific before general
An except clause also catches every subclass of its type. So put the more specific types first:
If the clauses were in the order except B, except C, except D, it would print B B B: the first matching clause wins.
โ Catches everything, hides bugs
โ Catch what you expect
โ ๏ธ Pitfall: a bare except: or except BaseException: also catches KeyboardInterrupt and SystemExit, so your program can't even be stopped. If you really must catch "anything", catch Exception, and log or re-raise it.
The else clause
Code in else runs only if the try block raised nothing. It keeps the try block small, so you don't accidentally catch errors from code you didn't mean to protect.
EAFP: "it's easier to ask forgiveness than permission"
Python code often tries an operation and handles the failure, rather than checking every condition first. Checking first is called "look before you leap", or LBYL; the try-first style is called EAFP. EAFP avoids race conditions (the file could vanish between the check and the open), and it is often shorter.
LBYL (Java style)
EAFP (Pythonic)
(For this simple case, d.get("fun", 0) is shorter than either.)
Raising exceptions
raise throws an exception: either an instance like ValueError("message"), or a class, which Python instantiates for you.
A bare raise inside an except block re-raises the current exception. Use it when you want to do something (such as logging) and still let the error continue upwards.
โ Java: Python has no checked exceptions and no throws clause. Any function may raise anything; document the important ones in the docstring. The keywords map as throw โ raise and catch โ except.
Exception chaining
If a new exception is raised while another one is being handled, Python links them and shows both: "During handling of the above exception, another exception occurred". To say on purpose "this error was caused by that one", use raise NewError(...) from original.
raise ... from None hides the original exception, for when it is an internal detail the user shouldn't see.
User-defined exceptions
Create your own exception types by subclassing Exception (classes are covered in Chapter 11). End the name in Error. A small hierarchy lets callers catch everything from your app with one clause, or catch just the specific cases.
Clean-up actions: finally
A finally block always runs as the try statement is left: after success, after a handled exception, after an unhandled one (which is then re-raised after finally), and even after return, break or continue.
What happened in try | except runs? | else runs? | finally runs? |
|---|---|---|---|
| no exception | no | yes | yes |
| exception, caught | yes (the matching one) | no | yes |
| exception, not caught | no | no | yes, then the exception continues |
return inside try | no | no | yes, just before returning |
โ ๏ธ Pitfall: a return, break or continue inside finally silently swallows any exception and overrides the try block's return value. Since 3.14, Python warns about it (SyntaxWarning: 'return' in a 'finally' block, PEP 765). Don't do it.
Predefined clean-up actions: with
Some objects know how to clean up after themselves. Files close themselves, and locks release themselves. The with statement guarantees that clean-up happens, so it replaces most try/finally blocks.
Objects that work with with are called context managers. You can write your own with contextlib.contextmanager, which you'll see in Chapter 12's generators.
Raising and handling several unrelated exceptions
Sometimes several independent things fail at once: validating a whole form, or running several tasks in parallel. ExceptionGroup bundles several exceptions together, and except* handles the ones of each type in the group.
Unlike plain except, several except* clauses can run for one group, one per matching type. Any exceptions left unhandled are re-raised in a smaller group.
Enriching exceptions with notes
err.add_note(text) attaches extra context to an exception after it was created. The notes are printed in the traceback, below the message.
More examples
1. Loading a file that may not exist yet
2. Retrying a flaky operation
3. Converting a whole column and reporting every bad row
4. A timing block with finally
5. Converting low-level errors into domain errors
Recap
- Syntax errors stop parsing. Exceptions happen at run time. Read the last line of the traceback first.
try/except Type as ecatches the first matching type, including its subclasses, so order the clauses specific โ general. Never use a bareexcept:.elseruns if nothing was raised.finallyalways runs; don'treturninside it.raise X(...)throws, a bareraisere-raises, andraise X from echains. Custom exceptions subclassException.withhandles clean-up.ExceptionGroupwithexcept*handles several errors at once, andadd_note()adds context.
Exercises
Exercise 1 ๐ข easy predict the output
Which exception does each line raise? int("3.5"), [1, 2][5], {"a": 1}["b"], len(5), undefined_name
Show solution
ValueError, IndexError, KeyError, TypeError, NameError. Note that int("3.5") fails: int() parses whole numbers only, so use float() first.
Exercise 2 ๐ข easy build it
Write safe_div(a, b) that returns a / b, or None when b is zero.
Show solution
Catch only the error you expect. A TypeError from bad input should still be raised.
Exercise 3 ๐ข easy fix the bug
Pressing Ctrl+C can't stop this loop. Why? Fix it.
Show solution
A bare except: also catches KeyboardInterrupt. Catch only ValueError, which is what int() raises for bad text.
Exercise 4 ๐ข easy predict the output
What does this print?
Show solution
A, B, D, E. No exception was raised, so except is skipped, else runs, and finally always runs.
Exercise 5 ๐ก medium predict the output
What does this print?
Show solution
cleanup then try. The return value is computed first, then finally runs, and then the function actually returns.
Exercise 6 ๐ก medium refactor it
This handler catches errors from process() by accident. Move the code so that only the conversion is protected.
Show solution
The else clause holds the code that should run only on success, without putting it under the same except.
Exercise 7 ๐ก medium build it ยท playground
In playground/errors.py, create TrackerError, with InvalidAmountError and EmptyCategoryError as subclasses. Write parse_line(line) that turns "12.5,food" into (12.5, "food"), raises the right error for bad input, and chains the original ValueError.
Show solution
str.partition(",") always returns 3 parts, even when there is no comma, so the unpacking never fails.
Exercise 8 ๐ก medium explain why
Why should you write except FileNotFoundError instead of first checking os.path.exists(path) and then opening the file?
Show solution
Between the check and the open, another program could delete or create the file. That is called a race condition. Opening and handling the error does both in one step (EAFP). It also covers other failures, like permissions, if you catch OSError.
Exercise 9 ๐ด hard predict the output
What does this print?
Show solution
V 2 then T 1. Each except* receives a sub-group containing all the exceptions of its type, and several clauses can run.
Exercise 10 ๐ด hard fix the bug
This function never reports errors. Why? Fix it.
Show solution
A return in finally replaces both the try block's result and any exception, so the function always returns "". Python 3.14+ warns about it. Catch the specific error instead.
Exercise 11 ๐ด hard mini-project ยท playground
Create playground/validate_all.py. Validate a list of expense dicts, collecting every problem as an exception, with a note added saying which record it came from. Raise them all as one ExceptionGroup. Handle that group with except*, printing the value errors and the key errors separately, with their notes.
Show solution
Collecting errors instead of stopping at the first one lets the user fix everything in one go. The notes live in e.__notes__.