Python Exceptions and Context Managers
- Description:
try/except/else/finally,raiseand exception chaining, the exception hierarchy, custom exceptions, thewithstatement and writing custom context managers,contextlib, andassert - My Notion Note ID: K2A-D1-12
- Created: 2023-01-10
- Updated: 2026-05-11
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1.
try/except/else/finally - 2.
raiseand Re-Raising - 3. Exception Chaining:
raise ... from ... - 4. The Exception Hierarchy
- 5. Custom Exceptions
- 6.
withand Context Managers - 7. Writing a Context Manager
- 8.
contextlib - 9.
assert - 10.
ExceptionGroupandexcept*
1. try / except / else / finally
try:
data = json.loads(text)
except json.JSONDecodeError as e:
log.warning("bad json: %s", e)
data = {}
except (OSError, ValueError) as e: # multiple types in one except
raise
else:
log.info("parsed ok") # only when try succeeded
finally:
cleanup() # always runs
Clause meanings:
-
except, match an exception -
else, run if no exception was raised (keep success-path code here so unrelated raises don't get caught) -
finally, always runs (success, exception, orreturn) -
Order
exceptclauses most-specific → most-general -
Bare
except:also catchesKeyboardInterruptandSystemExit, useexcept Exception:if you mean "any error"
2. raise and Re-Raising
raise ValueError("bad input") # raise a fresh instance
raise ValueError # shorthand for ValueError()
try:
...
except ValueError:
log.error(...)
raise # re-raise (preserves traceback)
raisewith no arg insideexcept, re-raises the active exception with its original traceback
3. Exception Chaining: raise ... from ...
try:
parse(text)
except ValueError as e:
raise ConfigError("invalid config") from e
fromsets__cause__→ traceback shows "this exception is because of that one"raise X from Nonesuppresses chaining- Implicit chaining also happens: an exception raised inside
exceptgets the active exception as__context__;fromoverrides it
4. The Exception Hierarchy
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception ← catch this, not BaseException
├── ArithmeticError
│ ├── ZeroDivisionError
│ └── OverflowError
├── AttributeError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── OSError # was IOError, EnvironmentError
│ ├── FileNotFoundError
│ ├── PermissionError
│ ├── TimeoutError
│ └── ...
├── RuntimeError
│ ├── RecursionError
│ └── NotImplementedError
├── StopIteration
├── TypeError
├── ValueError
│ └── UnicodeError
└── ...
- Rule of thumb: catch
Exceptionat top-level boundaries; catch specific subclasses inside library code
5. Custom Exceptions
class ConfigError(Exception):
"""Raised when configuration is malformed or missing."""
class MissingField(ConfigError):
def __init__(self, field: str):
super().__init__(f"missing required field: {field}")
self.field = field
- Add a docstring and put structured data in
__init__so callers canexcepton the class and read attributes - Avoid subclassing
BaseException, bypasses the standard "catch all" filter
6. with and Context Managers
- Guarantees cleanup, even on exception, Python's RAII
with open("data.txt") as f:
process(f.read())
# f is closed here whether process() succeeded or raised
Multiple context managers (3.10+ allows parens for line breaks):
with (
open("in.txt") as fin,
open("out.txt", "w") as fout,
lock,
):
fout.write(transform(fin.read()))
Common managers in the stdlib:
| Context manager | Resource managed |
|---|---|
open(...) |
File handle |
threading.Lock() |
Mutex |
tempfile.TemporaryDirectory() |
Temp dir (auto-removed) |
contextlib.suppress(SomeError) |
Swallow the named exception |
decimal.localcontext() |
Decimal context |
7. Writing a Context Manager
Implement __enter__ and __exit__:
class Timer:
def __enter__(self):
self.t0 = time.perf_counter()
return self # bound to `as` target
def __exit__(self, exc_type, exc, tb):
self.elapsed = time.perf_counter() - self.t0
# return True to swallow the exception; False/None to propagate
with Timer() as t:
run_slow_thing()
print(t.elapsed)
__exit__is called even when an exception is propagating,exc_type/exc/tbare non-Nonein that case
8. contextlib
Decorator form, usually shorter than a full class:
from contextlib import contextmanager
@contextmanager
def chdir(path):
old = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old)
with chdir("/tmp"):
do_work()
yieldseparates setup from teardown- Wrap in
try/finallyif cleanup must run on exceptions
Other useful tools:
from contextlib import suppress, closing, ExitStack, nullcontext
with suppress(FileNotFoundError):
os.remove("maybe.tmp") # no error if missing
with closing(urllib.urlopen(url)) as page:
... # calls page.close() on exit
with ExitStack() as stack:
files = [stack.enter_context(open(p)) for p in paths]
# all files closed on exit, in reverse order
ExitStack, for dynamic numbers of resources
9. assert
assert n >= 0, "n must be non-negative"
- Removed under
-O, never useassertfor validation that must survive in production - Use for invariants and tests only
10. ExceptionGroup and except*
PEP 654 (3.11+), wraps multiple concurrent exceptions into one group; except* peels them off by type.
try:
await asyncio.gather(t1, t2, t3)
except* ValueError as eg:
log.warning("value errors: %s", eg.exceptions)
except* OSError as eg:
log.error("io errors: %s", eg.exceptions)
- Most application code can ignore this until working heavily with concurrent tasks