Exceptions
|
This section documents the current Python release line as published at the official Python documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Python signals errors by raising exceptions rather than returning error codes, and handles them with
try/except. The full model is covered in the tutorial’s
Errors and Exceptions chapter and the
built-in exceptions reference.
try / except / else / finally
A try block runs its body; if an exception is raised, the first matching except clause runs instead.
else runs only when the body raised nothing; finally always runs, whether or not an exception
occurred:
def read_first_line(path):
try:
f = open(path)
except FileNotFoundError:
print(f"no such file: {path}")
return None
else:
try:
return f.readline()
finally:
f.close() # always runs: cleans up even if readline() raises
Catch Specific Types, Not Bare except:
Name the exception type(s) you expect. A bare except: (or except Exception: used carelessly) also
swallows programming errors — typos, KeyboardInterrupt, SystemExit — that should propagate instead
of being silently hidden:
def to_int(text):
try:
return int(text)
except ValueError: # only the error int() actually raises
return None
def risky():
try:
return 1 / 0
except: # AVOID: catches everything, including Ctrl-C
return None
A single except clause can name several types as a tuple, and multiple except clauses run in order,
the first match wins:
def parse(value):
try:
return int(value)
except (TypeError, ValueError) as exc:
print(f"bad input: {exc}")
return 0
The full statement grammar is documented under Handling Exceptions.
raise, Re-Raising, and Exception Chaining
raise with no argument, inside an except block, re-raises the exception currently being handled — useful for logging before letting it propagate:
def load_config(path):
try:
return int(open(path).read())
except ValueError:
print(f"malformed config at {path}")
raise # re-raise the same exception, unchanged
raise X from Y
Raising a new exception from inside an except block automatically chains it to the original as
_context_, shown as "During handling of the above exception, another exception occurred". Use
raise X from Y to make that link explicit — or raise X from None to suppress it when the original
is just noise:
def get_user(user_id, db):
try:
return db[user_id]
except KeyError as exc:
raise LookupError(f"no user with id {user_id}") from exc # explicit __cause__
def parse_strict(text):
try:
return int(text)
except ValueError:
raise ValueError(f"not a number: {text!r}") from None # hide the low-level cause
Chaining is documented in the same Errors and Exceptions chapter, under "Exception Chaining".
Custom Exception Classes
Subclass Exception (never BaseException directly) to define errors specific to your code. A small
hierarchy lets callers catch broadly or narrowly:
class AppError(Exception):
"""Base class for all errors raised by this application."""
class ValidationError(AppError):
"""Input failed validation."""
def __init__(self, field, reason):
super().__init__(f"{field}: {reason}")
self.field = field
self.reason = reason
class NotFoundError(AppError):
"""A requested resource does not exist."""
def get_item(items, key):
if key not in items:
raise NotFoundError(f"no item {key!r}")
return items[key]
try:
get_item({}, "widget")
except AppError as exc: # catches ValidationError, NotFoundError, and any future subclass
print(f"request failed: {exc}")
Custom exceptions are covered alongside the built-in hierarchy at the exceptions reference and in the tutorial’s User-defined Exceptions section.
Exception Groups (3.11+) and assert
Exception Groups and except*
An ExceptionGroup bundles several unrelated exceptions raised together — typically from concurrent
or batch operations where more than one failure can happen independently. except* matches groups by
the type of the exceptions inside them, and can run several handlers against the same group:
def validate_all(records):
errors = []
for i, record in enumerate(records):
if not record.get("name"):
errors.append(ValueError(f"record {i}: missing name"))
if record.get("age", 0) < 0:
errors.append(TypeError(f"record {i}: negative age"))
if errors:
raise ExceptionGroup("validation failed", errors)
try:
validate_all([{"age": -1}, {"name": ""}])
except* ValueError as eg:
print(f"value errors: {[str(e) for e in eg.exceptions]}")
except* TypeError as eg:
print(f"type errors: {[str(e) for e in eg.exceptions]}")
Each except* clause only "peels off" the matching sub-exceptions; any left unmatched propagate in a
new group. Full semantics are in
Raising
and Handling Multiple Unrelated Exceptions.
assert
assert raises AssertionError if its condition is falsy — intended for catching programmer errors
and invariants during development, not for validating user input, since assertions are stripped when
Python runs with -O:
def half(n):
assert n % 2 == 0, f"{n} is not even"
return n // 2
half(4) # 2
half(5) # AssertionError: 5 is not even
assert is documented under
the assert statement in the
language reference.
Control Flow Through try/except/else/finally
else and finally are easy to place wrong. else runs only on the no-exception path; finally runs
on every path, including one that exits via return, break, or an unhandled exception:
The full execution order — including what happens when finally itself contains a return — is
specified in
the Errors and Exceptions chapter and
the built-in exceptions reference.
See Also
-
Files and Context Managers —
withblocks as a cleaner alternative to manualtry/finallycleanup. -
Unit Testing with pytest & Mocking — asserting expected exceptions in tests instead of a bare
assert.