try / except
Wrap code that might fail in a try block. If an exception is raised, Python jumps to the matching except clause and runs it. Execution continues after the entire try/except structure.
1# Basic form2try:3 result = 10 / 04except ZeroDivisionError:5 print("cannot divide by zero")67# Catch multiple exception types separately8def parse_int(value):9 try:10 return int(value)11 except ValueError:12 print(f"not a valid integer: {value!r}")13 return None14 except TypeError:15 print(f"expected a string, got {type(value).__name__}")16 return None1718print(parse_int("42")) # 4219print(parse_int("hello")) # not a valid integer: 'hello'20print(parse_int(None)) # expected a string, got NoneType
Accessing the exception object
1try:2 data = {"key": "value"}3 print(data["missing"])4except KeyError as e:5 print(f"key not found: {e}") # key not found: 'missing'6 print(f"type: {type(e).__name__}") # type: KeyError78# Catching multiple types in one clause9try:10 result = int("bad") + None11except (ValueError, TypeError) as e:12 print(f"{type(e).__name__}: {e}")
else and finally
The else block runs only if no exception was raised. finally always runs, exception or not, making it the right place for cleanup code.
1def read_config(path):2 f = None3 try:4 f = open(path)5 data = f.read()6 except FileNotFoundError:7 print(f"config not found: {path}")8 return {}9 else:10 print("config loaded successfully")11 return data # only reached if no exception12 finally:13 if f:14 f.close() # always runs, even if return is hit in else
In practice, use with open(path) as f instead of manual finally: f.close(). Context managers handle this automatically.
Exception hierarchy
Python exceptions form a class hierarchy. except SomeType catches that type and all its subclasses. Catching Exception catches almost everything, but not SystemExit, KeyboardInterrupt, or GeneratorExit, which all inherit from BaseException directly. This is intentional. Those three should almost never be silenced.
1# Catching a parent catches all children2try:3 open("missing.txt")4except OSError as e:5 # catches FileNotFoundError, PermissionError, etc.6 print(type(e).__name__, e) # FileNotFoundError: ...78# Specific before general, Python checks clauses top to bottom9try:10 result = {}["key"]11except KeyError:12 print("specific: key not found")13except Exception:14 print("general: something went wrong") # not reached1516# Never do this, swallows everything including bugs17try:18 do_something()19except: # bare except catches BaseException, avoid20 pass
Click any exception class below to see exactly what that except clause would catch.
Click an exception class to see what except ExcType would catch.
Raising exceptions
Use raise to signal that something went wrong. Pick the most specific built-in exception that fits, or define a custom one. A clear exception with a descriptive message saves hours of debugging.
1def set_age(age: int) -> None:2 if not isinstance(age, int):3 raise TypeError(f"age must be int, got {type(age).__name__}")4 if age < 0 or age > 150:5 raise ValueError(f"age must be between 0 and 150, got {age}")67set_age(25) # fine8set_age("old") # TypeError: age must be int, got str9set_age(-1) # ValueError: age must be between 0 and 150, got -1
raise from, chaining exceptions
raise NewException from original attaches the original exception as the cause. The traceback shows both. This is important for library code: translate low-level exceptions into domain-level ones without hiding the root cause.
1class DatabaseError(Exception):2 pass34def get_user(user_id: int):5 try:6 # simulated DB lookup7 raw = {"1": "Alice"}[str(user_id)]8 return raw9 except KeyError as e:10 raise DatabaseError(f"user {user_id} not found") from e1112try:13 get_user(99)14except DatabaseError as e:15 print(e) # user 99 not found16 print(e.__cause__) # '99', the original KeyError
Custom exceptions
Define custom exceptions by subclassing Exception. Group related exceptions under a base class so callers can catch them broadly or specifically.
1class AppError(Exception):2 """Base class for all application errors."""34class ValidationError(AppError):5 def __init__(self, field: str, message: str):6 self.field = field7 super().__init__(f"{field}: {message}")89class NotFoundError(AppError):10 def __init__(self, resource: str, id):11 super().__init__(f"{resource} with id={id} not found")1213def get_product(product_id: int):14 if product_id <= 0:15 raise ValidationError("product_id", "must be positive")16 if product_id > 100:17 raise NotFoundError("Product", product_id)18 return {"id": product_id, "name": "Widget"}1920try:21 get_product(999)22except ValidationError as e:23 print(f"validation: {e.field}, {e}")24except NotFoundError as e:25 print(f"not found: {e}")26except AppError as e:27 print(f"app error: {e}") # catches any other AppError
Re-raising
1import logging23def process(data):4 try:5 return int(data)6 except ValueError as e:7 logging.error("failed to parse data: %s", e)8 raise # re-raise the same exception, preserving traceback910# Or raise a different exception11def load(path):12 try:13 with open(path) as f:14 return f.read()15 except OSError as e:16 raise RuntimeError(f"failed to load {path}") from e
Context managers
The with statement guarantees cleanup. Whatever happens inside the block, normal exit or an exception, the context manager's exit code always runs. This replaces the try/finally pattern for resource management.
1# File, most common use case2with open("data.txt", "w") as f:3 f.write("hello")4# file is closed here, even if write raised56# Multiple context managers on one line (Python 3.10+ prefers parentheses)7with open("input.txt") as src, open("output.txt", "w") as dst:8 dst.write(src.read().upper())
contextlib.contextmanager
The @contextmanager decorator lets you write a context manager as a generator. Everything before the yield is setup (called on entry), the yielded value becomes the as variable, and everything after is teardown (called on exit).
1from contextlib import contextmanager2import time34@contextmanager5def timer(label: str):6 start = time.perf_counter()7 try:8 yield # block runs here9 finally:10 elapsed = time.perf_counter() - start11 print(f"{label}: {elapsed:.4f}s")1213with timer("sorting"):14 data = sorted(range(1_000_000), reverse=True)1516# Timer prints even if the body raises17@contextmanager18def managed_connection(host: str):19 conn = connect(host) # setup20 try:21 yield conn # body gets the connection22 except Exception:23 conn.rollback()24 raise25 finally:26 conn.close() # always runs
contextlib utilities
1from contextlib import suppress, nullcontext23# suppress, silently ignore specific exceptions4with suppress(FileNotFoundError):5 import os6 os.remove("temp.txt") # no error if the file does not exist78# nullcontext, placeholder when you conditionally need a context manager9def process(file=None):10 ctx = open(file) if file else nullcontext()11 with ctx as f:12 data = f.read() if f else get_default_data()13 return data
Step through what happens inside a with block when the body succeeds or raises.
cm.__enter__()Context manager enters. Resources are acquired.
Patterns
EAFP vs LBYL
Two philosophies for dealing with conditions that might fail. LBYL (Look Before You Leap) checks before acting. EAFP (Easier to Ask Forgiveness than Permission) just tries and catches the failure. Python culture favors EAFP. It is often more readable and avoids race conditions.
1# LBYL, check first2def get_value_lbyl(d, key):3 if key in d: # check4 return d[key] # then act5 return None67# EAFP, try and catch8def get_value_eafp(d, key):9 try:10 return d[key] # try11 except KeyError: # handle failure12 return None1314# EAFP has fewer race conditions in concurrent code:15# between "if file exists" and "open file", the file can be deleted16# LBYL (prone to TOCTOU):17import os18if os.path.exists("data.txt"):19 with open("data.txt") as f: # file could be gone now20 data = f.read()2122# EAFP (safe):23try:24 with open("data.txt") as f:25 data = f.read()26except FileNotFoundError:27 data = None
Graceful degradation
Catch exceptions at the right level, where you have enough context to handle them meaningfully. Catch too low and you suppress useful errors. Catch too high and you lose precision.
1import json2import logging34def load_user_config(path: str) -> dict:5 try:6 with open(path) as f:7 return json.load(f)8 except FileNotFoundError:9 logging.info("no config at %s, using defaults", path)10 return {}11 except json.JSONDecodeError as e:12 logging.warning("invalid config at %s: %s", path, e)13 return {}1415# The caller gets a clean dict regardless, it never sees the exception16config = load_user_config("~/.myapp.json")
Exception groups (Python 3.11+)
1# Python 3.11 introduced ExceptionGroup for handling multiple exceptions2# raised concurrently (e.g., from asyncio task groups)34try:5 raise ExceptionGroup("multiple failures", [6 ValueError("bad value"),7 TypeError("wrong type"),8 ])9except* ValueError as eg:10 print("value errors:", eg.exceptions)11except* TypeError as eg:12 print("type errors:", eg.exceptions)
What not to do
1# Bad, bare except swallows everything2try:3 risky()4except:5 pass67# Bad, catching Exception too broadly hides bugs8try:9 result = complex_calculation()10except Exception:11 result = 0 # was it a real 0 or a bug?1213# Bad, using exceptions for normal control flow14def find_index(items, target):15 try:16 return items.index(target)17 except ValueError:18 return -1 # just use list.index() + a check, or next()