Skip to content

Context Managers

Ensure proper resource cleanup with context managers.

Context managers in Python are used to manage resources, ensuring they are properly acquired and released. They are typically used with the with statement and help prevent resource leaks.

Without context managers (error-prone):

bad_file_handling.py
file = open('file.txt', 'r')
try:
content = file.read()
finally:
file.close() # Must remember to close!

With context managers (pythonic):

good_file_handling.py
with open('file.txt', 'r') as file:
content = file.read()
# File is automatically closed after the block

A context manager needs to implement __enter__ and __exit__ methods:

database_context.py
class DatabaseConnection:
"""Custom context manager for database connections"""
def __init__(self, connection_string: str):
self.connection_string = connection_string
self.connection = None
def __enter__(self):
"""Called when entering the 'with' block"""
print("Connecting to database...")
self.connection = self.connect_to_database()
return self.connection
def __exit__(self, exc_type, exc_value, traceback):
"""Called when exiting the 'with' block"""
print("Closing database connection...")
if self.connection:
self.connection.close()
return False # Don't suppress exceptions
def connect_to_database(self):
# Simulate database connection
return {"status": "connected"}
# Usage
with DatabaseConnection("postgresql://localhost/db") as db:
print("Using database:", db)
# Database operations here
# Connection automatically closed

Function-Based Context Manager (using contextlib)

Section titled “Function-Based Context Manager (using contextlib)”

The contextlib module provides a decorator for creating context managers:

contextlib_example.py
from contextlib import contextmanager
@contextmanager
def database_connection(connection_string: str):
"""Function-based context manager"""
print("Connecting to database...")
connection = connect_to_database(connection_string)
try:
yield connection # Value returned to 'as' variable
finally:
print("Closing database connection...")
connection.close()
def connect_to_database(connection_string: str):
return {"status": "connected"}
# Usage
with database_connection("postgresql://localhost/db") as db:
print("Using database:", db)
timer_context.py
import time
from contextlib import contextmanager
@contextmanager
def timer():
"""Context manager to measure execution time"""
start = time.time()
try:
yield
finally:
end = time.time()
print(f"Execution time: {end - start:.2f} seconds")
# Usage
with timer():
# Your code here
time.sleep(1)
print("Task completed")
# Output: Execution time: 1.00 seconds
temp_directory.py
import tempfile
import shutil
from contextlib import contextmanager
@contextmanager
def temporary_directory():
"""Create and clean up a temporary directory"""
temp_dir = tempfile.mkdtemp()
try:
yield temp_dir
finally:
shutil.rmtree(temp_dir) # Clean up
# Usage
with temporary_directory() as temp_dir:
# Work with temp_dir
print(f"Working in {temp_dir}")
# Directory automatically deleted
lock_context.py
import threading
from contextlib import contextmanager
@contextmanager
def acquire_lock(lock: threading.Lock):
"""Acquire and release a lock"""
lock.acquire()
try:
yield
finally:
lock.release()
# Usage
lock = threading.Lock()
with acquire_lock(lock):
# Critical section
print("Thread-safe code here")
# Lock automatically released

The __exit__ method receives three parameters about any exception that occurred:

exception_handling.py
class ErrorLogger:
"""Context manager that logs exceptions"""
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is not None:
print(f"Exception occurred: {exc_type.__name__}: {exc_value}")
# Return False to propagate the exception
# Return True to suppress it
return False
# Usage
with ErrorLogger():
raise ValueError("Something went wrong")
# Exception is logged but still raised

You can use multiple context managers in one statement:

multiple_contexts.py
from contextlib import contextmanager
import time
@contextmanager
def timer():
start = time.time()
try:
yield
finally:
print(f"Time: {time.time() - start:.2f}s")
@contextmanager
def logger(name: str):
print(f"Starting {name}...")
try:
yield
finally:
print(f"Finished {name}")
# Multiple context managers
with timer(), logger("operation"):
time.sleep(1)
print("Doing work...")