Exceptions
Context Managers
Automatic Cleanup
You open a file, read it, then must close it. Forgetting to close leaks resources.
Context managers with with automatically close resources when done - even if
exceptions occur. No more manual try-finally for cleanup.
Basic with statement
Let Python handle resource cleanup.
# Basic with statement usage
def main():
# Simulated file handling
class FakeFile:
def __init__(self, name, mode):
self.name = name
self.mode = mode
self.closed = False
def __enter__(self):
print(f" Opening {self.name}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f" Closing {self.name}")
self.closed = True
return False # Don't suppress exceptions
def read(self):
if self.closed:
raise ValueError("File is closed")
return f"Contents of {self.name}"
print("Basic with statement:\n")
# Using with statement
with FakeFile("data.txt", "r") as f:
content = f.read()
print(f" Read: {content}")
print(f" File closed: {f.closed}")
# Without with statement (manual)
print("\nManual resource management:")
f2 = FakeFile("manual.txt", "r")
f2.__enter__()
try:
content = f2.read()
print(f" Read: {content}")
finally:
f2.__exit__(None, None, None)
# Multiple context managers
print("\nMultiple resources:")
with FakeFile("file1.txt", "r") as f1, FakeFile("file2.txt", "r") as f2:
print(f" Reading {f1.name}")
print(f" Reading {f2.name}")
print(" Both files closed")
# Exception handling
print("\nWith exception:")
try:
with FakeFile("error.txt", "r") as f:
print(f" Inside with block")
raise ValueError("Simulated error")
except ValueError as e:
print(f" Caught: {e}")
print(f" File was still closed: {f.closed}")
# Timer context
import time
class Timer:
def __enter__(self):
self.start = time.time()
print(" Timer started")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.time() - self.start
print(f" Timer stopped: {self.elapsed:.4f}s")
return False
print("\nTiming code:")
with Timer():
# Simulate work
total = sum(range(1000))
print(f" Computed sum: {total}")
if __name__ == "__main__":
main()
with open(file) as f: automatically closes file when block exits.
Class-based context manager
Create your own with enter and exit.
# Class-based context managers
def main():
# Simple resource manager
class Resource:
def __init__(self, name):
self.name = name
self.acquired = False
def __enter__(self):
print(f" Acquiring {self.name}")
self.acquired = True
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f" Releasing {self.name}")
self.acquired = False
return False # Propagate exceptions
def use(self):
if not self.acquired:
raise RuntimeError("Resource not acquired")
print(f" Using {self.name}")
print("Resource management:\n")
with Resource("Database") as db:
db.use()
print(f" Acquired: {db.acquired}")
print(f" After with: {db.acquired}")
# Context manager with return value
class Connection:
def __init__(self, host, port):
self.host = host
self.port = port
self.connected = False
def __enter__(self):
print(f" Connecting to {self.host}:{self.port}")
self.connected = True
return self # Returns self for use in 'as' clause
def __exit__(self, exc_type, exc_val, exc_tb):
print(f" Disconnecting from {self.host}:{self.port}")
self.connected = False
return False
def send(self, data):
if not self.connected:
raise RuntimeError("Not connected")
print(f" Sent: {data}")
print("\nConnection:")
with Connection("localhost", 8080) as conn:
conn.send("Hello")
conn.send("World")
# Exception information in __exit__
class ErrorHandler:
def __enter__(self):
print(" Entering context")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
print(" Exiting normally")
else:
print(f" Exception occurred: {exc_type.__name__}")
print(f" Message: {exc_val}")
return False # Don't suppress
print("\nError handling:")
with ErrorHandler():
print(" No error")
print()
try:
with ErrorHandler():
print(" About to raise error")
raise ValueError("Test error")
except ValueError:
print(" Exception propagated")
# Suppress exception by returning True
class Suppressor:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ValueError:
print(f" Suppressing {exc_type.__name__}: {exc_val}")
return True # Suppress ValueError
return False # Propagate other exceptions
print("\nSuppressing exceptions:")
with Suppressor():
print(" Raising ValueError")
raise ValueError("This will be suppressed")
print(" Execution continued")
# Lock-like context manager
class Lock:
def __init__(self, name):
self.name = name
self.locked = False
def __enter__(self):
print(f" Acquiring lock: {self.name}")
self.locked = True
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f" Releasing lock: {self.name}")
self.locked = False
return False
print("\nLocking:")
lock = Lock("data_lock")
with lock:
print(f" Lock held: {lock.locked}")
# Critical section
print(f" Lock released: {lock.locked}")
if __name__ == "__main__":
main()
__enter__ sets up, __exit__ cleans up. Works with with statement.
Decorator-based context manager
Simpler syntax with @contextmanager.
# contextmanager decorator
from contextlib import contextmanager
def main():
# Simple context manager with decorator
@contextmanager
def managed_resource(name):
print(f" Setup: {name}")
yield name # Provide value to 'as' clause
print(f" Cleanup: {name}")
print("Decorator-based context manager:\n")
with managed_resource("Resource1") as res:
print(f" Using: {res}")
# Context manager with exception handling
@contextmanager
def safe_operation(name):
print(f" Starting: {name}")
try:
yield name
except Exception as e:
print(f" Error in {name}: {e}")
raise # Re-raise
finally:
print(f" Finishing: {name}")
print("\nWith exception handling:")
with safe_operation("Operation1"):
print(" Working...")
print()
try:
with safe_operation("Operation2"):
print(" Working...")
raise ValueError("Something went wrong")
except ValueError:
print(" Exception caught in main")
# Timer context manager
import time
@contextmanager
def timer(label):
start = time.time()
yield
elapsed = time.time() - start
print(f" {label}: {elapsed:.4f}s")
print("\nTiming:")
with timer("Computation"):
total = sum(range(100000))
print(f" Sum: {total}")
# Temporary directory simulation
@contextmanager
def temp_directory(name):
print(f" Creating temp dir: {name}")
dir_path = f"/tmp/{name}"
try:
yield dir_path
finally:
print(f" Removing temp dir: {name}")
print("\nTemporary directory:")
with temp_directory("work_dir") as path:
print(f" Using directory: {path}")
# Do work in temp directory
# Database transaction simulation
@contextmanager
def transaction(db_name):
print(f" BEGIN TRANSACTION on {db_name}")
try:
yield
print(f" COMMIT on {db_name}")
except Exception as e:
print(f" ROLLBACK on {db_name} ({e})")
raise
print("\nDatabase transactions:")
with transaction("users_db"):
print(" INSERT INTO users...")
print(" UPDATE users...")
print()
try:
with transaction("orders_db"):
print(" INSERT INTO orders...")
raise RuntimeError("Constraint violation")
except RuntimeError:
print(" Transaction rolled back")
# Changing context
@contextmanager
def working_directory(path):
original = "/current/dir"
print(f" Changing to: {path}")
try:
yield path
finally:
print(f" Restoring to: {original}")
print("\nWorking directory:")
with working_directory("/new/path") as path:
print(f" Working in: {path}")
if __name__ == "__main__":
main()
yield separates setup from cleanup. Code before yield runs on enter.
Multiple contexts
Manage several resources at once.
# Multiple context managers
from contextlib import contextmanager
def main():
# Nested with statements (old style)
@contextmanager
def resource(name):
print(f" Open: {name}")
yield name
print(f" Close: {name}")
print("Nested contexts (old style):\n")
with resource("Resource1"):
with resource("Resource2"):
with resource("Resource3"):
print(" Using all resources")
# Multiple contexts in one with (Python 3.1+)
print("\nMultiple in one with:")
with resource("R1"), resource("R2"), resource("R3"):
print(" Using all resources")
# File copy simulation
@contextmanager
def fake_file(name, mode):
print(f" Opening {name} ({mode})")
yield f"<{name} handle>"
print(f" Closing {name}")
print("\nFile copy:")
with fake_file("input.txt", "r") as src, fake_file("output.txt", "w") as dst:
print(f" Reading from {src}")
print(f" Writing to {dst}")
# Database connection and cursor
@contextmanager
def connection(db_name):
print(f" Connect to {db_name}")
yield f"<{db_name} connection>"
print(f" Disconnect from {db_name}")
@contextmanager
def cursor(conn):
print(f" Create cursor on {conn}")
yield f"<cursor>"
print(f" Close cursor")
print("\nDatabase operations:")
with connection("users_db") as conn:
with cursor(conn) as cur:
print(f" Execute query with {cur}")
# Cleanup order matters
class OrderedResource:
def __init__(self, name, order):
self.name = name
self.order = order
def __enter__(self):
print(f" [{self.order}] Enter: {self.name}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f" [{self.order}] Exit: {self.name}")
return False
print("\nCleanup order:")
with OrderedResource("First", 1), \
OrderedResource("Second", 2), \
OrderedResource("Third", 3):
print(" [X] All acquired")
# Exception during acquisition
@contextmanager
def failing_resource(name, should_fail=False):
print(f" Acquiring {name}")
if should_fail:
raise RuntimeError(f"{name} failed to acquire")
yield name
print(f" Releasing {name}")
print("\nException during acquisition:")
try:
with failing_resource("R1"), \
failing_resource("R2", should_fail=True), \
failing_resource("R3"):
print(" Won't reach here")
except RuntimeError as e:
print(f" Error: {e}")
print(" Note: R1 was released, R3 never acquired")
# Nested transactions
@contextmanager
def savepoint(name):
print(f" SAVEPOINT {name}")
try:
yield
print(f" RELEASE SAVEPOINT {name}")
except Exception as e:
print(f" ROLLBACK TO SAVEPOINT {name}")
raise
print("\nNested transactions:")
try:
with savepoint("sp1"):
print(" Operation 1")
with savepoint("sp2"):
print(" Operation 2")
raise ValueError("Error in sp2")
except ValueError:
print(" Rolled back to sp1")
if __name__ == "__main__":
main()
with open(a) as f1, open(b) as f2: - all cleaned up properly.
contextlib utilities
Helpful functions for common patterns.
# contextlib utilities
from contextlib import contextmanager, suppress, redirect_stdout, closing
import io
def main():
# suppress - ignore specific exceptions
print("Using suppress:\n")
from contextlib import suppress
# Without suppress
try:
int("not a number")
except ValueError:
pass # Silently ignore
print(" Attempt 1: Failed silently")
# With suppress
with suppress(ValueError):
int("not a number")
print(" Attempt 2: Failed silently")
# Suppress multiple exception types
with suppress(ValueError, TypeError, KeyError):
d = {}
value = d["missing_key"]
print(" Attempt 3: Suppressed KeyError")
# redirect_stdout - capture print output
print("\nRedirecting stdout:")
output = io.StringIO()
with redirect_stdout(output):
print("This goes to StringIO")
print("Not to console")
captured = output.getvalue()
print(f" Captured: {repr(captured)}")
# closing - ensure object is closed
class Resource:
def __init__(self, name):
self.name = name
self.closed = False
def close(self):
print(f" Closing {self.name}")
self.closed = True
print("\nUsing closing:")
with closing(Resource("res1")) as res:
print(f" Using {res.name}")
print(f" Closed: {res.closed}")
# nullcontext - conditional context manager
from contextlib import nullcontext
print("\nConditional context:")
use_lock = False
@contextmanager
def lock():
print(" Acquiring lock")
yield
print(" Releasing lock")
context = lock() if use_lock else nullcontext()
with context:
print(" Critical section (no lock)")
use_lock = True
context = lock() if use_lock else nullcontext()
with context:
print(" Critical section (with lock)")
# ExitStack - dynamic context managers
from contextlib import ExitStack
print("\nExitStack:")
@contextmanager
def file_context(name):
print(f" Open {name}")
yield name
print(f" Close {name}")
files = ["file1.txt", "file2.txt", "file3.txt"]
with ExitStack() as stack:
handles = [stack.enter_context(file_context(f)) for f in files]
print(f" All files opened: {handles}")
print(" All files closed")
# ExitStack with callbacks
print("\nExitStack with callbacks:")
with ExitStack() as stack:
stack.callback(lambda: print(" Callback 1"))
stack.callback(lambda: print(" Callback 2"))
print(" In context")
# Practical: resource pool
print("\nResource pool:")
def acquire_resources(count):
stack = ExitStack()
resources = []
try:
for i in range(count):
res = Resource(f"res{i}")
stack.enter_context(closing(res))
resources.append(res)
return stack.pop_all(), resources
except:
stack.close()
raise
with ExitStack() as stack:
mgr, resources = acquire_resources(3)
stack.enter_context(mgr)
print(f" Using {len(resources)} resources")
if __name__ == "__main__":
main()
suppress(), redirect_stdout(), closing() - ready-made context managers.
Exercise: practical.py
Build a database connection context manager