Chapter 10 ยท Modules, I/O & Errors

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 finally and with.
  • ExceptionGroup, except*, and add_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

ExceptionTypical cause
ValueErrorright type, wrong value: int("abc")
TypeErrorwrong type: "a" + 1, or calling with the wrong number of arguments
NameErrora name that was never defined (often a typo)
AttributeErrorno such attribute or method: [].push
IndexError / KeyErrorlist index out of range / dict key missing (both are LookupError)
ZeroDivisionErrordividing by zero (an ArithmeticError)
FileNotFoundError, PermissionErrorfile problems (both are OSError)
KeyboardInterruptthe 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:

  1. The try block runs.
  2. If no exception happens, all the except clauses are skipped.
  3. If an exception happens, the rest of try is skipped, and the first matching except runs.
  4. If no except matches, 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 tryexcept runs?else runs?finally runs?
no exceptionnoyesyes
exception, caughtyes (the matching one)noyes
exception, not caughtnonoyes, then the exception continues
return inside trynonoyes, 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 e catches the first matching type, including its subclasses, so order the clauses specific โ†’ general. Never use a bare except:.
  • else runs if nothing was raised. finally always runs; don't return inside it.
  • raise X(...) throws, a bare raise re-raises, and raise X from e chains. Custom exceptions subclass Exception.
  • with handles clean-up. ExceptionGroup with except* handles several errors at once, and add_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__.

Official docs