Errors and Context Managers
Python signals failure by raising exceptions, and expects you to let them propagate unless you can genuinely handle them. The with statement is the companion feature: it guarantees cleanup runs no matter how a block exits.
What an exception is
Section titled “What an exception is”An exception is an object that interrupts normal flow and unwinds the call stack until something catches it. Uncaught, it prints a traceback and exits with status 1.
def divide(a, b): return a / b
divide(1, 0)Traceback (most recent call last): File "app.py", line 4, in <module> divide(1, 0) File "app.py", line 2, in divide return a / b ~~^~~ZeroDivisionError: division by zeroRead tracebacks bottom-up: the last line is the error, the line above it is where it happened, and the frames above that are how you got there. Python 3.11+ adds the ~~^~~ markers pinpointing the failing sub-expression on the line, which is a large debugging improvement on long expressions.
try / except / else / finally
Section titled “try / except / else / finally”try: value = int(user_input)except ValueError: print("not a number") value = 0else: print("parsed cleanly")finally: print("always runs")| Clause | Runs when |
|---|---|
try |
Always — the protected code |
except |
A matching exception was raised |
else |
The try block completed with no exception |
finally |
Always, on the way out — including via return, break, or an uncaught exception |
else exists so the try block can stay as small as possible. Code that should not be protected goes in else:
try: conn = connect()except ConnectionError: return Noneelse: return conn.fetch() # a ConnectionError here is NOT swallowedfinally is for cleanup that must happen regardless:
f = open("data.txt", encoding="utf-8")try: process(f)finally: f.close()(In practice you would use with for exactly this — see below.)
Catching specific vs broad
Section titled “Catching specific vs broad”Catch the narrowest exception that you can actually do something about.
try: config = json.loads(text)except json.JSONDecodeError as exc: logger.error("bad config: %s", exc) config = DEFAULTSMultiple handlers are tested top to bottom; the first match wins, so order from specific to general:
try: ...except FileNotFoundError: ...except OSError: # would also catch FileNotFoundError — must come second ...except (ValueError, TypeError) as exc: # a tuple catches any of them ...try: risky()except Exception: logger.exception("risky() failed") # logs message + full traceback raise # re-raise, preserving the original tracebackThe exception object carries useful data:
try: open("missing.txt")except OSError as exc: exc.errno # => 2 exc.strerror # => 'No such file or directory' exc.filename # => 'missing.txt' exc.args # the constructor argumentsThe exception hierarchy
Section titled “The exception hierarchy”Every exception inherits from BaseException. The structure is what makes broad-but-safe catches possible.
BaseException├── SystemExit raised by sys.exit()├── KeyboardInterrupt Ctrl-C├── GeneratorExit└── Exception <- catch THIS, not BaseException ├── ArithmeticError │ ├── ZeroDivisionError │ └── OverflowError ├── AttributeError ├── EOFError ├── ImportError │ └── ModuleNotFoundError ├── LookupError │ ├── IndexError │ └── KeyError ├── NameError │ └── UnboundLocalError ├── OSError │ ├── FileNotFoundError │ ├── FileExistsError │ ├── PermissionError │ ├── IsADirectoryError │ ├── TimeoutError │ └── ConnectionError │ ├── ConnectionRefusedError │ ├── ConnectionResetError │ └── BrokenPipeError ├── RuntimeError │ ├── RecursionError │ └── NotImplementedError ├── StopIteration ├── SyntaxError │ └── IndentationError ├── TypeError ├── ValueError │ └── UnicodeDecodeError └── WarningThe three exceptions above Exception are deliberately outside it: SystemExit, KeyboardInterrupt, and GeneratorExit are control flow, not errors, and except Exception: correctly lets them through.
Choosing what to raise:
| Situation | Raise |
|---|---|
| Wrong type of argument | TypeError |
| Right type, unacceptable value | ValueError |
| Missing dict key | KeyError |
| Index out of range | IndexError |
| Filesystem / network failure | The relevant OSError subclass |
| Abstract method not overridden | NotImplementedError |
| Nothing else fits and the state is wrong | RuntimeError |
raise and re-raise
Section titled “raise and re-raise”raise ValueError("age must be non-negative")raise ValueError # instantiated for you, but give a messageA bare raise inside an except block re-raises the current exception with its original traceback intact:
try: parse()except ValueError: metrics.increment("parse_failures") raise # not `raise exc` — the bare form preserves everythingCustom exceptions
Section titled “Custom exceptions”Define your own so callers can catch your errors specifically. Give a library one base class so users can catch everything from it in one clause.
class AppError(Exception): """Base class for all errors from this application."""
class ConfigError(AppError): """Configuration is missing or invalid."""
class ValidationError(AppError): def __init__(self, field, message): self.field = field self.message = message super().__init__(f"{field}: {message}") # sets str(exc)try: load()except ValidationError as exc: print(exc.field)except AppError: print("something else in our app failed")Exception chaining
Section titled “Exception chaining”When you catch one exception and raise another, Python keeps the original attached so the traceback shows both.
Implicit chaining happens automatically when you raise inside an except block. The traceback reads “During handling of the above exception, another exception occurred”.
Explicit chaining with from states the causal relationship, and prints “The above exception was the direct cause of the following exception”:
try: config = json.loads(raw)except json.JSONDecodeError as exc: raise ConfigError("config file is not valid JSON") from excThe original is available as exc.__cause__ (explicit) or exc.__context__ (implicit).
from None suppresses the chain, when the inner exception is noise:
try: value = mapping[key]except KeyError: raise ConfigError(f"missing setting: {key}") from NoneException groups (3.11+)
Section titled “Exception groups (3.11+)”When several things fail together — concurrent tasks, batched validation — ExceptionGroup carries them all, and except* handles them by type.
try: raise ExceptionGroup("batch failed", [ValueError("a"), TypeError("b")])except* ValueError as eg: print("value errors:", eg.exceptions)except* TypeError as eg: print("type errors:", eg.exceptions)Both handlers run. This is what asyncio.TaskGroup raises when multiple tasks fail.
assert
Section titled “assert”assert condition, message raises AssertionError when the condition is falsy. It is a development-time sanity check, not error handling.
def average(xs): assert xs, "average() requires a non-empty sequence" return sum(xs) / len(xs)Use assert for invariants you believe cannot be violated, and raise for conditions you expect might occur.
The with statement
Section titled “The with statement”with binds a resource for the duration of a block and guarantees teardown, even on exception, return, or break.
with open("data.txt", encoding="utf-8") as f: for line in f: process(line)# the file is closed here, guaranteedMultiple resources in one statement, with parentheses for wrapping (Python 3.10+ formally allows the parenthesised form):
with ( open("in.txt", encoding="utf-8") as src, open("out.txt", "w", encoding="utf-8") as dst,): dst.write(src.read())Standard-library objects that are context managers include files, sockets, threading.Lock, database connections and cursors, tempfile.TemporaryDirectory, subprocess.Popen, decimal.localcontext, and unittest.mock.patch.
import threading, tempfile, pathlib
lock = threading.Lock()with lock: shared_state += 1 # released even if the body raises
with tempfile.TemporaryDirectory() as tmp: (pathlib.Path(tmp) / "scratch.txt").write_text("hi", encoding="utf-8")# the directory and its contents are deleted hereWriting one as a class
Section titled “Writing one as a class”A context manager is any object with __enter__ and __exit__.
class Timer: def __enter__(self): import time self.start = time.perf_counter() return self # this is what `as` binds
def __exit__(self, exc_type, exc_value, traceback): import time self.elapsed = time.perf_counter() - self.start print(f"took {self.elapsed:.4f}s") return False # falsy => propagate any exception
with Timer() as t: heavy_work()__exit__ receives the exception type, value, and traceback — all None if the block finished normally. Returning a truthy value suppresses the exception.
Selective suppression:
def __exit__(self, exc_type, exc_value, traceback): if exc_type is ZeroDivisionError: print("ignored a division by zero") return True return FalseWriting one with contextlib
Section titled “Writing one with contextlib”@contextlib.contextmanager turns a generator into a context manager: everything before yield is setup, the yielded value is bound by as, and everything after is teardown.
from contextlib import contextmanager
@contextmanagerdef timer(label): import time start = time.perf_counter() try: yield label finally: print(f"{label}: {time.perf_counter() - start:.4f}s")
with timer("query"): run_query()A realistic example — a transaction that commits or rolls back:
@contextmanagerdef transaction(conn): cur = conn.cursor() try: yield cur except Exception: conn.rollback() raise else: conn.commit() finally: cur.close()
with transaction(conn) as cur: cur.execute("INSERT INTO users (name) VALUES (?)", ("Ada",))Other contextlib tools
Section titled “Other contextlib tools”from contextlib import suppress, closing, redirect_stdout, ExitStackimport io
# Ignore a specific exception — clearer than try/except/passwith suppress(FileNotFoundError): pathlib.Path("temp.txt").unlink()
# Call .close() on an object that has close() but no __exit__with closing(legacy_resource()) as res: res.use()
# Capture printed outputbuf = io.StringIO()with redirect_stdout(buf): print("captured")buf.getvalue() # => 'captured\n'
# A variable number of context managerswith ExitStack() as stack: files = [stack.enter_context(open(p, encoding="utf-8")) for p in paths] # all closed on exit, in reverse ordercontextlib.AbstractContextManager is the ABC if you want to inherit; contextlib.asynccontextmanager is the async with equivalent, covered in typing and async.
Key points
Section titled “Key points”- Read tracebacks bottom-up; 3.11+ points at the exact failing sub-expression.
elseruns when nothing was raised,finallyalways runs — neverreturnfromfinally.- Catch the narrowest exception you can handle; never write a bare
except:. Exceptionis the safe broad catch;BaseExceptionincludes Ctrl-C andsys.exit().- A bare
raisere-raises with the original traceback;raise X from excrecords the cause. - Give each project one base exception class and derive the rest from it.
assertdisappears under-O— never use it for validation.withguarantees cleanup; write one via__enter__/__exit__or@contextmanagerwithtry/finally.