Python Context Managers & contextlib
The Python Context Manager Protocol powers the with statement, guaranteeing deterministic resource acquisition and release.
A class implements the protocol by defining two methods:
__enter__(self): Runs when entering thewithblock. Its return value is bound to the variable afteras.__exit__(self, exc_type, exc_val, exc_tb): Runs when exiting thewithblock. If an exception occurred inside the block, its details are passed into these arguments. ReturningTruefrom__exit__suppresses the exception; returningFalseallows it to propagate!
# Class-based Custom File / Resource Context Manager:
class ManagedFile:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
print(f"๐ [__enter__] Opening file: {self.filename}")
self.file = open(self.filename, self.mode, encoding="utf-8")
return self.file # Bound to 'as f'
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"๐ [__exit__] Closing file: {self.filename}")
if self.file:
self.file.close()
# Returning False allows any exception inside the block to propagate properly
return False
# Using custom context manager:
with ManagedFile("demo_context.txt", "w") as f:
f.write("Hello from custom ManagedFile context manager! ๐\n")
print("Is file closed?", f.closed) # True!
Even if a divide-by-zero or network error occurs inside the with block, __exit__ is guaranteed to execute and close the file descriptor.
Writing a full class with __enter__ and __exit__ for small tasks can feel verbose. The standard library @contextlib.contextmanager decorator turns any Python generator with a single yield into a complete context manager!
import time
from contextlib import contextmanager
# 1. Performance Stopwatch Context Manager in 8 lines:
@contextmanager
def code_timer(label="Operation"):
start = time.perf_counter()
print(f"โณ [{label}] Started...")
try:
yield # Body of with block executes here!
finally:
end = time.perf_counter()
duration_ms = (end - start) * 1000
print(f"โ [{label}] Finished in {duration_ms:.3f} ms")
# Using the generator context manager:
with code_timer("Data Processing Pipeline"):
# Simulate data computation:
results = [x ** 2 for x in range(300_000)]
time.sleep(0.05)
Always place the code after yield inside a finally: block to guarantee cleanup runs even if an exception occurs inside the with block.
If you write code after yield without wrapping it in a try-finally block, an exception inside the caller's with block will prevent your cleanup code from executing at all!
Build a context manager temporary_state(data_dict, key, temp_val) that sets a key to a temporary value inside the block and automatically restores the original value upon exit.
from contextlib import contextmanager
@contextmanager
def temporary_setting(config_dict, key, temp_val):
original_val = config_dict.get(key)
config_dict[key] = temp_val
try:
yield
finally:
config_dict[key] = original_val
settings = {"debug": False, "env": "production"}
print("Initial Settings:", settings)
with temporary_setting(settings, "debug", True):
print("Inside with block:", settings)
print("Outside with block:", settings)
Q How can a context manager suppress an exception?
In a class-based context manager, returning True from __exit__() informs Python that the exception has been handled and should not propagate.
Q Can multiple context managers be combined in a single with statement?
Yes! You can chain multiple context managers with commas: with open("in.txt") as f_in, open("out.txt", "w") as f_out:.
Q What is contextlib.suppress in Python?
contextlib.suppress(*exceptions) is a built-in context manager that silently suppresses specified exceptions: with contextlib.suppress(FileNotFoundError): os.remove("file.tmp").