Lesson 9 - Context Managers and Resource Management
On this page
- Introduction
- The Problem: Manual Resource Management
- The Solution: Context Managers
- How Context Managers Work
- Creating a Simple Context Manager
- Handling Exceptions in
__exit__ - Practical Example: Transaction Manager
- The
contextlibModule - Suppressing Exceptions with
contextlib.suppress - Redirecting Output with
contextlib.redirect_stdout - Nested Context Managers
- Practical Example: File Writer with Backup
- Practical Example: Book Catalog Manager
- Summary
Introduction
You’ve probably seen code like this:
with open('books.csv', 'r') as file:
content = file.read()The with statement ensures the file is properly closed, even if an error occurs. This is an example of a context manager—an object that sets up and tears down resources automatically.
Context managers are essential for:
- File operations
- Database connections
- Network connections
- Locks and synchronization
- Temporary state changes
In this lesson, we’ll learn:
- How the
withstatement works - Creating context managers with classes (
__enter__and__exit__) - Creating context managers with
@contextmanagerdecorator - Practical context manager examples
- Nested context managers
The Problem: Manual Resource Management
Without context managers, you have to manually clean up resources:
# Risky code - what if an exception occurs?
file = open('books.csv', 'r')
content = file.read()
# Process content...
file.close() # Might never execute if exception occurs!You should use try/finally:
file = open('books.csv', 'r')
try:
content = file.read()
# Process content...
finally:
file.close() # Always executesThis works but is verbose and easy to forget.
The Solution: Context Managers
Context managers handle setup and cleanup automatically:
with open('books.csv', 'r') as file:
content = file.read()
# Process content...
# File is automatically closed here, even if an exception occurredMuch cleaner!
How Context Managers Work
The with statement calls two special methods:
__enter__(): Called when entering thewithblock__exit__(): Called when leaving thewithblock (even if an exception occurs)
Here’s what happens:
with open('books.csv', 'r') as file:
content = file.read()
# Equivalent to:
file = open('books.csv', 'r')
file_obj = file.__enter__() # Returns self (the file object)
try:
content = file_obj.read()
finally:
file.__exit__(None, None, None) # Always calledCreating a Simple Context Manager
Let’s create a context manager that prints messages on enter/exit:
class BookstoreConnection:
"""Simulate a database connection"""
def __init__(self, connection_string):
self.connection_string = connection_string
self.connected = False
def __enter__(self):
"""Setup: Connect to database"""
print(f"Connecting to {self.connection_string}...")
self.connected = True
return self # Return value is assigned to 'as' variable
def __exit__(self, exc_type, exc_value, exc_traceback):
"""Cleanup: Disconnect from database"""
print("Closing connection...")
self.connected = False
# Return False to propagate exceptions (default)
return False
def query(self, sql):
"""Execute a query"""
if not self.connected:
raise RuntimeError("Not connected!")
print(f"Executing: {sql}")
return ["Book 1", "Book 2", "Book 3"]
# Use the context manager
with BookstoreConnection("bookstore.db") as db:
results = db.query("SELECT * FROM books")
print(f"Results: {results}")Output:
Connecting to bookstore.db...
Executing: SELECT * FROM books
Results: ['Book 1', 'Book 2', 'Book 3']
Closing connection...Handling Exceptions in __exit__
The __exit__ method receives exception information if an error occurs:
class ErrorHandler:
def __enter__(self):
print("Entering context")
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
if exc_type is not None:
print(f"An error occurred: {exc_type.__name__}: {exc_value}")
# Return True to suppress the exception
return True
print("Exiting normally")
return False
# Example 1: No error
print("=== No Error ===")
with ErrorHandler():
print("Doing some work...")
# Example 2: With error (suppressed)
print("\n=== With Error (Suppressed) ===")
with ErrorHandler():
print("Doing some work...")
raise ValueError("Something went wrong!")
print("Program continues!")Output:
=== No Error ===
Entering context
Doing some work...
Exiting normally
=== With Error (Suppressed) ===
Entering context
Doing some work...
An error occurred: ValueError: Something went wrong!
Program continues!Practical Example: Transaction Manager
Let’s create a context manager for database-like transactions:
class Transaction:
"""Manage database transactions"""
def __init__(self, db_name):
self.db_name = db_name
self.changes = []
def __enter__(self):
print(f"BEGIN TRANSACTION on {self.db_name}")
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
if exc_type is None:
# No exception - commit
print("COMMIT TRANSACTION")
print(f"Applied {len(self.changes)} changes")
else:
# Exception occurred - rollback
print("ROLLBACK TRANSACTION")
print(f"Discarded {len(self.changes)} changes")
self.changes.clear()
# Don't suppress exceptions
return False
def add_book(self, title):
"""Add a book (simulated change)"""
self.changes.append(('INSERT', title))
print(f" Added: {title}")
def remove_book(self, title):
"""Remove a book (simulated change)"""
self.changes.append(('DELETE', title))
print(f" Removed: {title}")
# Example 1: Successful transaction
print("=== Successful Transaction ===")
with Transaction("bookstore.db") as txn:
txn.add_book("Python Basics")
txn.add_book("Python Advanced")
# Example 2: Failed transaction (automatically rolls back)
print("\n=== Failed Transaction ===")
try:
with Transaction("bookstore.db") as txn:
txn.add_book("Data Science 101")
txn.add_book("Machine Learning")
raise RuntimeError("Network error!")
except RuntimeError:
print("Handled the error")Output:
=== Successful Transaction ===
BEGIN TRANSACTION on bookstore.db
Added: Python Basics
Added: Python Advanced
COMMIT TRANSACTION
Applied 2 changes
=== Failed Transaction ===
BEGIN TRANSACTION on bookstore.db
Added: Data Science 101
Added: Machine Learning
ROLLBACK TRANSACTION
Discarded 2 changes
Handled the errorThe contextlib Module
Python’s contextlib module provides tools for working with context managers.
The @contextmanager Decorator
Instead of creating a class, you can use a generator function:
from contextlib import contextmanager
@contextmanager
def bookstore_connection(connection_string):
"""Context manager using a generator"""
# Setup (before yield)
print(f"Connecting to {connection_string}...")
conn = {"connected": True, "name": connection_string}
try:
yield conn # Value passed to 'as' variable
finally:
# Cleanup (after yield)
print("Closing connection...")
conn["connected"] = False
# Use it
with bookstore_connection("bookstore.db") as db:
print(f"Using connection: {db}")Output:
Connecting to bookstore.db...
Using connection: {'connected': True, 'name': 'bookstore.db'}
Closing connection...This is much simpler than creating a class!
Practical Example: Timing Context Manager
from contextlib import contextmanager
import time
@contextmanager
def timer(name):
"""Time a block of code"""
start_time = time.time()
print(f"Starting {name}...")
try:
yield
finally:
end_time = time.time()
duration = end_time - start_time
print(f"{name} took {duration:.4f} seconds")
# Use it
with timer("Book Processing"):
# Simulate some work
books = []
for i in range(1000000):
books.append(i ** 2)
time.sleep(0.5)Output:
Starting Book Processing...
Book Processing took 0.5234 secondsPractical Example: Temporary Directory Changer
from contextlib import contextmanager
import os
@contextmanager
def change_directory(new_dir):
"""Temporarily change working directory"""
old_dir = os.getcwd()
print(f"Changing directory: {old_dir} → {new_dir}")
try:
os.chdir(new_dir)
yield
finally:
print(f"Restoring directory: {new_dir} → {old_dir}")
os.chdir(old_dir)
# Example usage
print(f"Current directory: {os.getcwd()}")
with change_directory("/tmp"):
print(f"Inside with block: {os.getcwd()}")
# Do work in /tmp...
print(f"After with block: {os.getcwd()}")Suppressing Exceptions with contextlib.suppress
from contextlib import suppress
# Instead of try/except for specific exceptions
books = ["Python Basics", "Python Advanced"]
# Without suppress
try:
books.remove("Data Science 101") # Not in list
except ValueError:
pass
# With suppress (cleaner)
with suppress(ValueError):
books.remove("Machine Learning") # Not in list
print(books)Redirecting Output with contextlib.redirect_stdout
from contextlib import redirect_stdout
import io
# Redirect print output to a string
output = io.StringIO()
with redirect_stdout(output):
print("This goes to the string")
print("So does this")
# Get the captured output
result = output.getvalue()
print(f"Captured: {result}")Output:
Captured: This goes to the string
So does thisNested Context Managers
You can use multiple context managers:
from contextlib import contextmanager
@contextmanager
def transaction(name):
print(f"BEGIN {name}")
try:
yield
finally:
print(f"END {name}")
# Nested with statements
with transaction("Outer"):
print(" Outer work")
with transaction("Inner"):
print(" Inner work")
print(" More outer work")Output:
BEGIN Outer
Outer work
BEGIN Inner
Inner work
END Inner
More outer work
END OuterYou can also write them on one line:
with open('input.txt', 'r') as infile, open('output.txt', 'w') as outfile:
content = infile.read()
outfile.write(content.upper())Practical Example: File Writer with Backup
Let’s create a context manager that creates a backup before writing:
from contextlib import contextmanager
import os
import shutil
@contextmanager
def safe_write(filename):
"""Context manager that backs up file before writing"""
backup_name = f"{filename}.backup"
# Create backup if file exists
if os.path.exists(filename):
print(f"Creating backup: {backup_name}")
shutil.copy2(filename, backup_name)
try:
# Open file for writing
with open(filename, 'w') as f:
yield f
# Success - remove backup
if os.path.exists(backup_name):
print(f"Removing backup: {backup_name}")
os.remove(backup_name)
except Exception as e:
# Error - restore from backup
print(f"Error occurred: {e}")
if os.path.exists(backup_name):
print(f"Restoring from backup: {backup_name}")
shutil.move(backup_name, filename)
raise
# Use it
with safe_write('books.txt') as f:
f.write("Python Basics\n")
f.write("Python Advanced\n")Practical Example: Book Catalog Manager
Let’s build a comprehensive example combining everything we’ve learned:
from contextlib import contextmanager
import time
class BookCatalog:
"""Manage book catalog with context managers"""
def __init__(self, name):
self.name = name
self.books = []
self.locked = False
@contextmanager
def transaction(self):
"""Context manager for transactions"""
print(f"[{self.name}] BEGIN TRANSACTION")
original_books = self.books.copy()
changes = []
try:
# Yield a helper object for making changes
class TransactionHelper:
def add_book(helper_self, book):
self.books.append(book)
changes.append(('ADD', book))
print(f" Added: {book}")
def remove_book(helper_self, book):
if book in self.books:
self.books.remove(book)
changes.append(('REMOVE', book))
print(f" Removed: {book}")
yield TransactionHelper()
# Commit
print(f"[{self.name}] COMMIT ({len(changes)} changes)")
except Exception as e:
# Rollback
self.books = original_books
print(f"[{self.name}] ROLLBACK ({len(changes)} changes)")
raise
@contextmanager
def lock(self):
"""Context manager for locking catalog"""
if self.locked:
raise RuntimeError("Catalog is already locked")
print(f"[{self.name}] LOCK acquired")
self.locked = True
try:
yield
finally:
self.locked = False
print(f"[{self.name}] LOCK released")
def display(self):
"""Display catalog contents"""
print(f"\n{self.name} Catalog ({len(self.books)} books):")
for i, book in enumerate(self.books, 1):
print(f" {i}. {book}")
# Use the catalog with context managers
catalog = BookCatalog("Main Store")
# Successful transaction
print("=== Adding Books ===")
with catalog.lock():
with catalog.transaction() as txn:
txn.add_book("Python Basics")
txn.add_book("Python Advanced")
txn.add_book("Data Science 101")
catalog.display()
# Failed transaction (automatically rolls back)
print("\n=== Attempting Bad Transaction ===")
try:
with catalog.transaction() as txn:
txn.add_book("Machine Learning")
txn.add_book("Deep Learning")
raise ValueError("Inventory check failed!")
except ValueError as e:
print(f"Transaction failed: {e}")
catalog.display()
# Removing a book
print("\n=== Removing a Book ===")
with catalog.transaction() as txn:
txn.remove_book("Python Advanced")
catalog.display()Output:
=== Adding Books ===
[Main Store] LOCK acquired
[Main Store] BEGIN TRANSACTION
Added: Python Basics
Added: Python Advanced
Added: Data Science 101
[Main Store] COMMIT (3 changes)
[Main Store] LOCK released
Main Store Catalog (3 books):
1. Python Basics
2. Python Advanced
3. Data Science 101
=== Attempting Bad Transaction ===
[Main Store] BEGIN TRANSACTION
Added: Machine Learning
Added: Deep Learning
[Main Store] ROLLBACK (2 changes)
Transaction failed: Inventory check failed!
Main Store Catalog (3 books):
1. Python Basics
2. Python Advanced
3. Data Science 101
=== Removing a Book ===
[Main Store] BEGIN TRANSACTION
Removed: Python Advanced
[Main Store] COMMIT (1 changes)
Main Store Catalog (2 books):
1. Python Basics
2. Data Science 101Summary
In this lesson, we learned about context managers:
- Context managers handle resource setup and cleanup automatically
- The
withstatement uses__enter__and__exit__methods __exit__is always called, even if an exception occurs- Class-based: Implement
__enter__and__exit__methods - Function-based: Use
@contextmanagerdecorator with generators contextlibmodule provides useful context manager utilities
Common uses:
- File operations
- Database transactions
- Locks and synchronization
- Temporary state changes
- Timing and profiling
- Exception suppression
Benefits:
- Automatic cleanup (no forgotten
close()calls) - Exception-safe (cleanup happens even with errors)
- Cleaner code (no try/finally boilerplate)
- Reusable resource management patterns
Best practices:
- Use context managers for any resource that needs cleanup
- Prefer
@contextmanagerfor simple cases - Use classes for complex state management
- Don’t suppress exceptions unless intentional (return False from
__exit__)
In the next lesson, we’ll explore regular expressions—a powerful tool for text processing and pattern matching.