Context Managers, try/finally & Resource Safety
Resource management in Python relies on Context Managers via the with statement protocol (__enter__ and __exit__). Context managers guarantee deterministic resource setup and cleanup (file handles, database transactions, network sockets, thread locks), preventing resource leaks even when unhandled exceptions occur.
This chapter details the with statement execution protocol, BEFORE_WITH / SETUP_WITH opcodes, exception suppression mechanics via __exit__, contextlib helpers, and re-entrant context managers.
1. The Context Manager Protocol (__enter__ & __exit__)
Any Python object implementing __enter__() and __exit__() is a context manager.
When Python executes a with manager as target: block:
The Context Manager Execution Sequence:
[ Evaluate with manager expression ]
|
v
[ Call manager.__enter__() ] <-- Setup resource; return value bound to target variable
|
v
[ Execute 'with' Block Body ]
|
+---> Body Completes Normally?
| ├── YES: Call manager.__exit__(None, None, None)
| └── NO: An Exception occurred!
| v
| Call manager.__exit__(exc_type, exc_val, exc_tb)
| |
| +---> __exit__ returns True?
| ├── YES: Suppress Exception! (Execution continues after with block)
| └── NO: Re-raise Exception! (Propagates up stack)Protocol Signature:
__enter__(self): Acquires resource; returns bound variable (as target).__exit__(self, exc_type, exc_val, exc_tb): Releases resource. If an exception occurred inside thewithbody, arguments contain exception details; otherwise all three areNone.
2. Exception Suppression Mechanics
If an exception occurs inside the with block, __exit__() receives the exception instance. If __exit__() returns True, Python suppresses the exception, preventing it from propagating up the stack. Returning False (or None) allows the exception to propagate normally.
class SuppressValueError:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ValueError:
print(f"Suppressed ValueError: {exc_val}")
return True # SUPPRESS EXCEPTION! Execution continues smoothly.
return False # Allow other exceptions to propagate3. Generator Context Managers (contextlib.contextmanager)
Creating class-based context managers for simple resource setups involves boilerplate. contextlib.contextmanager converts a generator function into a context manager using a single yield:
from contextlib import contextmanager
import time
@contextmanager
def timer(label: str):
start = time.perf_counter()
try:
yield # Execution yields control to 'with' block body
finally:
elapsed = time.perf_counter() - start
print(f"[{label}] Elapsed: {elapsed:.4f}s") # GUARANTEED CLEANUP!Execution Mapping:
- Code before
yieldcorresponds to__enter__(). - The yielded value is bound to the
as targetvariable. - Code inside
finally:corresponds to__exit__().
4. Re-entrant vs. Non-Re-entrant Context Managers
- Re-entrant Context Managers: Can be reused in multiple nested
withblocks (e.g.threading.RLock). - Non-Re-entrant Context Managers: State bound to a single execution (e.g.
open()file objects). Re-using an opened file context manager in a nested block raisesValueError: I/O operation on closed file.