Context Managers¶
Resource management is a recurring challenge in programming: files must be closed, locks must be released, and database connections must be returned to the pool — even when an exception interrupts normal flow. Python's with statement and the context manager protocol guarantee that cleanup code runs no matter what. Any object that implements __enter__ and __exit__ can be used with with.
Mental Model
A context manager is a pair of bookends: __enter__ sets things up and __exit__ tears them down, guaranteed. No matter what happens between them -- even if an exception fires -- the cleanup code runs. Whenever you find yourself writing try/finally, a context manager is the Pythonic replacement.
With Statement Support¶
1. The enter Method¶
The __enter__ method is called when execution enters the with block. It performs any setup work (opening a file, acquiring a lock) and returns the resource that will be bound to the variable after the as keyword. If no resource needs to be returned, it typically returns self.
2. The exit Method¶
The __exit__ method is called when execution leaves the with block, whether normally or because of an exception. It receives three arguments — exc_type, exc_val, and exc_tb — that describe the exception, or are all None if the block completed successfully. After performing cleanup, __exit__ can suppress the exception by returning True, or let it propagate by returning False (or None).
3. Usage¶
The following class wraps a filename and ensures the underlying file handle is always closed, even if an error occurs inside the with block.
```python class ManagedFile: def init(self, filename): self.filename = filename
def __enter__(self):
self.file = open(self.filename, "r")
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
return False # Do not suppress exceptions
with ManagedFile("example.txt") as f: contents = f.read()
f is guaranteed to be closed here¶
```
When the with block begins, __enter__ opens the file and returns the file handle. When the block ends — normally or via exception — __exit__ closes the file.
with vs try/finally¶
Context managers replace the try/finally pattern. Both guarantee cleanup, but with is shorter and harder to get wrong:
```python
try/finally — correct but verbose¶
f = open("data.txt") try: data = f.read() finally: f.close()
with — same guarantee, less code¶
with open("data.txt") as f: data = f.read() ```
As resource management grows more complex (nested resources, exception handling, conditional cleanup), try/finally becomes error-prone while with stays clean. Context managers also serve as a design pattern analogous to RAII (Resource Acquisition Is Initialization) in C++. See also Callable Objects, Containers, and Iteration for the other core protocols.
Be Careful with Exception Suppression
If __exit__ returns True, the exception is suppressed — execution continues normally after the with block as if nothing went wrong. This is a powerful but dangerous capability. Most context managers should return False (or None) to let exceptions propagate. Suppressing exceptions can hide bugs and make debugging extremely difficult. Only suppress exceptions when you have a clear, documented reason (e.g., contextlib.suppress).
Common Use Cases
Context managers are the standard pattern for:
- File I/O — open/close
- Database transactions — begin/commit/rollback
- Thread locks — acquire/release
- Network connections — connect/disconnect
- Temporary state changes — redirect stdout, change working directory
- Resource pooling — acquire from pool, return to pool
Summary¶
- Context managers guarantee resource cleanup by pairing setup (
__enter__) with teardown (__exit__). - The
__exit__method always runs, even when an exception occurs inside thewithblock. - Prefer
withovertry/finally--- it is shorter, safer, and composable. - Implement
__enter__and__exit__on any class to make its instances usable with thewithstatement. - Most context managers should not suppress exceptions — return
Falsefrom__exit__unless you have a specific reason to suppress.
When NOT to Use a Context Manager
Don't use a context manager when there is no setup/teardown symmetry. If your object only needs setup (initialization) or only needs cleanup (finalization), a context manager adds complexity without benefit. Similarly, avoid context managers when the resource's lifecycle is not scoped — if the resource must outlive the with block, a context manager forces an awkward pattern. Use explicit open()/close() methods instead, or manage lifecycle at a higher level.
Runnable Example: context_manager_protocol.py¶
```python """ TUTORIAL: Context Managers - The enter and exit Protocol
This tutorial teaches you how to implement the context manager protocol by defining enter and exit methods. Context managers enable the 'with' statement, which is Python's way of managing resource setup and cleanup safely and reliably.
Real-world uses: file handling, database connections, locks, temporary redirects, transaction management, and anything that needs setup/teardown.
Key Learning Goals: - Understand when and why context managers matter - Implement enter and exit properly - Handle exceptions in exit - See how the 'with' statement works under the hood """
import sys
if name == "main":
print("=" * 70)
print("TUTORIAL: Context Managers - __enter__ and __exit__")
print("=" * 70)
# ============ EXAMPLE 1: The Problem Context Managers Solve ============
print("\n# Example 1: Resource Cleanup Without Context Managers")
print("=" * 70)
print("""
Imagine you have a resource that needs setup and teardown:
WRONG WAY (without context manager):
resource = acquire_resource()
resource.do_work()
resource.cleanup() # Oops! What if do_work() raises an exception?
# cleanup() never runs!
SAFER WAY (manual try/finally):
resource = acquire_resource()
try:
resource.do_work()
finally:
resource.cleanup() # Always runs, even if exception occurs
PYTHONIC WAY (context manager):
with acquire_resource() as resource:
resource.do_work() # cleanup() automatically happens after
The 'with' statement guarantees cleanup happens, even on exceptions.
It's cleaner, safer, and more readable than try/finally.
""")
# ============ EXAMPLE 2: The Context Manager Protocol ============
print("\n# Example 2: Understanding __enter__ and __exit__")
print("=" * 70)
print("""
A context manager is any object with two dunder methods:
class ContextManager:
def __enter__(self):
# Called when entering the with block
# Setup goes here
# Return a value (or None) to assign to 'as variable'
return something
def __exit__(self, exc_type, exc_value, traceback):
# Called when exiting the with block (always!)
# Cleanup goes here
# If it returns True, the exception is suppressed
# If it returns False or None, exception propagates
return False # Don't suppress exceptions
When you write:
with obj:
# ... code ...
Python does this:
mgr = obj
exit = type(mgr).__exit__
value = type(mgr).__enter__(mgr)
try:
# ... code ...
except:
exc_info = sys.exc_info()
if not exit(mgr, *exc_info):
raise # Exception propagates
else:
exit(mgr, None, None, None) # Normal exit
""")
# ============ EXAMPLE 3: A Simple Example - Stdout Redirection ============
print("\n# Example 3: The LookingGlass Context Manager")
print("=" * 70)
class LookingGlass:
"""
A context manager that reverses stdout output (mirror effect).
This is a playful example but demonstrates the protocol perfectly.
While active, all print output is reversed - it appears backwards.
"""
def __enter__(self):
"""
Called when entering the with block.
Tasks:
1. Save the original sys.stdout.write method
2. Replace it with our reverse_write method
3. Return a value to be assigned to 'as variable'
This is the setup phase.
"""
# Save the original so we can restore it later
self.original_write = sys.stdout.write
# Replace with our custom function
sys.stdout.write = self.reverse_write
# Return a value for the 'as' variable
return 'JABBERWOCKY'
def reverse_write(self, text):
"""
Our custom stdout.write that reverses text.
This will be called by print() internally, so output appears
backwards. We call the original write with reversed text.
"""
self.original_write(text[::-1])
def __exit__(self, exc_type, exc_value, traceback):
"""
Called when exiting the with block (always!).
Parameters:
exc_type: Exception class if an exception occurred (None otherwise)
exc_value: Exception instance if one occurred (None otherwise)
traceback: Exception traceback if one occurred (None otherwise)
Tasks:
1. Restore the original stdout.write
2. Optionally handle exceptions (return True to suppress)
Return value:
True: suppress the exception (don't re-raise)
False/None: let exception propagate normally
"""
# Always restore the original write method
sys.stdout.write = self.original_write
# Handle ZeroDivisionError specially
if exc_type is ZeroDivisionError:
print('Please DO NOT divide by zero!')
return True # Suppress this specific exception
# All other exceptions propagate normally (implicit return None)
# Demonstrate the context manager
print("Before entering context manager:")
print("This text is printed normally")
print()
print("Entering context manager with LookingGlass():")
with LookingGlass() as what:
print('Alice, Kitty and Snowdrop')
print(f"The variable 'what' is: {what}")
print()
print("After exiting context manager:")
print("Output is back to normal")
print("""
WHY: The context manager handles all the complexity for you.
You don't have to remember try/finally or manual cleanup.
The 'with' statement makes it impossible to forget.
""")
# ============ EXAMPLE 4: Understanding the Execution Order ============
print("\n# Example 4: Step-by-Step Execution")
print("=" * 70)
class Tracer:
"""Context manager that logs when things happen."""
def __enter__(self):
print(" 1. __enter__ called (setup phase)")
return "Inside the with block"
def __exit__(self, exc_type, exc_value, traceback):
print(" 3. __exit__ called (cleanup phase)")
if exc_type is not None:
print(f" Exception occurred: {exc_type.__name__}")
return False # Don't suppress exceptions
print("Normal execution:")
with Tracer() as var:
print(f" 2. Inside with block, var = {var}")
print()
print("Execution with exception:")
try:
with Tracer() as var:
print(f" 2. Inside with block")
x = 1 / 0 # Raise exception
except ZeroDivisionError:
print(" 4. Exception propagated and caught by outer try/except")
print("""
KEY INSIGHT: __exit__ is ALWAYS called, whether the block completes
normally or an exception occurs. This guarantee is what makes context
managers so powerful for cleanup.
""")
# ============ EXAMPLE 5: Handling Exceptions ============
print("\n# Example 5: Exception Handling in __exit__")
print("=" * 70)
class ErrorHandler:
"""
Context manager that can suppress certain exceptions.
Shows how returning True from __exit__ suppresses exceptions.
"""
def __init__(self, exception_type):
self.exception_type = exception_type
def __enter__(self):
print(f" __enter__: Will suppress {self.exception_type.__name__}")
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
print(" __exit__: Normal exit (no exception)")
return False
elif exc_type is self.exception_type:
print(f" __exit__: Caught {exc_type.__name__} - suppressing it")
return True # Suppress this exception
else:
print(f" __exit__: Caught {exc_type.__name__} - NOT suppressing")
return False # Don't suppress
print("Suppressing ValueError:")
with ErrorHandler(ValueError):
print(" Inside block, raising ValueError")
raise ValueError("This will be suppressed")
print(" Block exited - exception was suppressed!")
print()
print("NOT suppressing TypeError:")
try:
with ErrorHandler(ValueError):
print(" Inside block, raising TypeError")
raise TypeError("This will NOT be suppressed")
except TypeError:
print(" Block exited - exception propagated and caught!")
print("""
WHY: Some context managers (like pytest fixtures) suppress expected
exceptions. Returning True says "I handled this, don't propagate."
Returning False or None says "I didn't handle this, propagate it."
Most context managers return None or False - they don't suppress.
""")
# ============ EXAMPLE 6: Real-World Example - File Handling ============
print("\n# Example 6: File Handling (Real-World Context Manager)")
print("=" * 70)
class ManagedFile:
"""
A context manager for file operations.
This demonstrates why context managers matter in real code:
files need to be opened and closed reliably.
"""
def __init__(self, name, mode):
self.name = name
self.mode = mode
self.file = None
def __enter__(self):
print(f" __enter__: Opening file '{self.name}'")
self.file = open(self.name, self.mode)
return self.file
def __exit__(self, exc_type, exc_value, traceback):
if self.file:
print(f" __exit__: Closing file '{self.name}'")
self.file.close()
if exc_type is not None:
print(f" __exit__: Exception occurred: {exc_type.__name__}")
return False
# Demonstrate file handling
print("Writing to a file:")
with ManagedFile('/tmp/test.txt', 'w') as f:
f.write("Hello, World!\n")
f.write("This is a test file.\n")
print()
print("Reading from the file:")
with ManagedFile('/tmp/test.txt', 'r') as f:
for line in f:
print(f" Read: {line.rstrip()}")
print()
print("File closed automatically after each with block!")
print("""
WHY: Built-in file objects already implement the context manager protocol,
so you can use:
with open('file.txt') as f:
# ...
This is so common that it's the Pythonic way to open files. Files are
automatically closed when exiting the with block, even if an exception
occurs.
""")
# ============ EXAMPLE 7: Multiple Resources ============
print("\n# Example 7: Managing Multiple Resources")
print("=" * 70)
class Resource:
"""Simulates a resource that needs cleanup."""
def __init__(self, name):
self.name = name
def __enter__(self):
print(f" Acquired {self.name}")
return self
def __exit__(self, exc_type, exc_value, traceback):
print(f" Released {self.name}")
return False
def use(self):
print(f" Using {self.name}")
print("Managing multiple resources with nested with statements:")
with Resource("Resource A") as a:
with Resource("Resource B") as b:
with Resource("Resource C") as c:
a.use()
b.use()
c.use()
print()
print("More readable: use comma syntax (Python 3.10+):")
print("""
with Resource("A") as a, \\
Resource("B") as b, \\
Resource("C") as c:
a.use()
b.use()
c.use()
Resources are acquired and released in the correct order (LIFO - Last In,
First Out). C is released first, then B, then A.
""")
# ============ EXAMPLE 8: The Complete Pattern ============
print("\n# Example 8: Context Manager Checklist")
print("=" * 70)
print("""
When implementing a context manager, follow this pattern:
class MyContextManager:
def __init__(self, ...):
# Store initialization parameters
self.resource = None
def __enter__(self):
# 1. Acquire resource
self.resource = acquire_resource()
# 2. Perform any setup
self.resource.setup()
# 3. Return a value for 'as variable' (or self)
return self.resource
def __exit__(self, exc_type, exc_value, traceback):
# 1. Always clean up resources
if self.resource:
self.resource.cleanup()
# 2. Optional: handle specific exceptions
if exc_type is SomeException:
# Handle it
return True # Suppress
# 3. Return False/None to propagate exceptions
return False
USAGE:
with MyContextManager(...) as resource:
resource.do_something()
# Cleanup guaranteed to happen here
KEY RULES:
- __enter__ runs when entering the with block
- __exit__ ALWAYS runs when exiting (normal or exception)
- __exit__ receives exception info (or None, None, None if normal)
- Return True to suppress exception, False/None to propagate
- Use __exit__ for cleanup, not for normal work
""")
# ============ EXAMPLE 9: Common Pattern - Lock Management ============
print("\n# Example 9: Lock Management (Concurrency Pattern)")
print("=" * 70)
class SimpleLock:
"""Context manager for thread synchronization."""
def __init__(self, name):
self.name = name
self.acquired = False
def __enter__(self):
print(f" Acquiring lock: {self.name}")
self.acquired = True
return self
def __exit__(self, exc_type, exc_value, traceback):
if self.acquired:
print(f" Releasing lock: {self.name}")
self.acquired = False
print("Lock acquisition and release:")
with SimpleLock("data_lock"):
print(" Critical section protected by lock")
print(" Can safely access shared data")
print(" Lock automatically released")
print("""
WHY: Context managers are essential for thread-safe code. Locks are
acquired on __enter__ and released on __exit__, even if exceptions occur.
This prevents deadlocks and data corruption.
This pattern is used in threading.Lock, threading.RLock, and many
concurrency libraries.
""")
# ============ EXAMPLE 10: Summary ============
print("\n# Example 10: When to Use Context Managers")
print("=" * 70)
print("""
USE CONTEXT MANAGERS FOR:
1. File operations (open, read, write, close)
2. Database transactions (begin, commit, rollback)
3. Network connections (connect, disconnect)
4. Thread locks (acquire, release)
5. Temporary state changes (redirect stdout, change directory)
6. Resource pooling (acquire from pool, return to pool)
7. Timing operations (start timer, stop and report)
8. Error suppression (catch specific exceptions)
PATTERN CHECKLIST:
✓ __enter__ acquires resources and returns them
✓ __exit__ releases resources (cleanup)
✓ __exit__ always runs, even on exceptions
✓ Suppressing exceptions is optional (usually don't)
✓ Use 'with' statement (never call __enter__/__exit__ directly)
✓ Can stack multiple with statements for multiple resources
BENEFITS:
✓ Guaranteed cleanup (no forgotten cleanup calls)
✓ Clean, readable code
✓ Exception-safe
✓ Clear intent (anyone sees 'with' knows resource is managed)
✓ Pythonic (idiomatic Python style)
NEXT: If you find context managers verbose, learn about @contextmanager
decorator in the next tutorial - it simplifies many common cases!
""")
print("\n" + "=" * 70)
print("KEY TAKEAWAYS")
print("=" * 70)
print("""
1. CONTEXT MANAGERS ARE FOR RESOURCE MANAGEMENT: Use them whenever
something needs setup and cleanup (files, locks, transactions, etc).
2. __enter__ AND __exit__ ARE THE PROTOCOL: Implement these two methods
and any object becomes a context manager usable with 'with'.
3. __exit__ IS GUARANTEED TO RUN: This is what makes context managers
so powerful - cleanup is guaranteed even if exceptions occur.
4. RETURN VALUE MATTERS: Return True to suppress exceptions, False/None
to propagate them. Most context managers propagate.
5. WITH STATEMENT IS CLEANER: 'with' is more readable and safer than
manual try/finally. Always prefer 'with' when possible.
6. USE THE PROTOCOL EVERYWHERE APPLICABLE: Any resource that needs
cleanup should be managed through a context manager. It's a sign
of professional Python code.
7. YOU'RE PROBABLY ALREADY USING THEM: File objects, database
connections, and many libraries use context managers. Now you can
build your own!
""")
```
Runnable Example: context_manager_decorator.py¶
```python """ TUTORIAL: The @contextmanager Decorator - Simpler Context Managers
This tutorial teaches you how to use @contextmanager decorator from the contextlib module. Instead of writing classes with enter and exit methods, you can write a simple generator function that's much more concise.
This is the Pythonic way to create context managers when you don't need the full power of a class.
Key Learning Goals: - Understand how @contextmanager transforms generators into context managers - Write context managers as functions instead of classes - Learn when to use decorator vs class approach - Master the yield pattern for setup/teardown """
import contextlib import sys
if name == "main":
print("=" * 70)
print("TUTORIAL: @contextmanager Decorator - Simpler Context Managers")
print("=" * 70)
# ============ EXAMPLE 1: Why the Decorator Exists ============
print("\n# Example 1: Class vs Function Approaches")
print("=" * 70)
print("""
REMINDER: Context managers can be classes:
class LookingGlass:
def __enter__(self):
self.original = sys.stdout.write
sys.stdout.write = self.reverse_write
return 'JABBERWOCKY'
def reverse_write(self, text):
self.original(text[::-1])
def __exit__(self, exc_type, exc_value, traceback):
sys.stdout.write = self.original
return False
This is verbose and has a lot of boilerplate. If your context manager
just does setup (before), then cleanup (after), there's a simpler way.
WITH @contextmanager:
@contextlib.contextmanager
def looking_glass():
original = sys.stdout.write
sys.stdout.write = lambda text: original(text[::-1])
try:
yield 'JABBERWOCKY'
finally:
sys.stdout.write = original
Much more concise! The decorator handles __enter__ and __exit__ for you.
""")
# ============ EXAMPLE 2: Basic @contextmanager Usage ============
print("\n# Example 2: Your First @contextmanager")
print("=" * 70)
@contextlib.contextmanager
def simple_context():
"""
A simple context manager using the decorator.
The pattern:
1. Do setup before yield
2. Yield a value to return from __enter__
3. Do cleanup after yield (in finally block)
The decorator automatically:
- Wraps the yield value in a context manager
- Runs code before yield in __enter__
- Runs code after yield in __exit__
- Handles exceptions properly
"""
print(" Entering context (setup)")
yield "setup complete"
print(" Exiting context (cleanup)")
print("Using the context manager:")
with simple_context() as value:
print(f" Inside with block, value = {value}")
print()
print("Notice how clean this is:")
print(" - No class definition needed")
print(" - No __enter__ and __exit__ methods")
print(" - Setup and cleanup are adjacent in the code")
print("""
WHY: For simple setup/cleanup patterns, the decorator is more readable.
The yield statement clearly marks what gets returned and when cleanup happens.
""")
# ============ EXAMPLE 3: The LookingGlass with Decorator ============
print("\n# Example 3: LookingGlass Revisited (Much Simpler)")
print("=" * 70)
@contextlib.contextmanager
def looking_glass():
"""
Reverse stdout output using the decorator.
Compare this to the class version in the previous tutorial.
This is so much simpler!
HOW IT WORKS:
1. Save original sys.stdout.write
2. Yield the magic word to be returned by __enter__
3. In finally block, restore the original write
4. Everything between yield and end of function runs in __exit__
"""
original_write = sys.stdout.write
def reverse_write(text):
original_write(text[::-1])
sys.stdout.write = reverse_write
try:
yield 'JABBERWOCKY' # This value is returned by __enter__
finally:
sys.stdout.write = original_write # Always runs in __exit__
print("Before context:")
print("Normal output here")
print()
with looking_glass() as word:
print('Alice, Kitty and Snowdrop')
print(f"The magic word: {word}")
print()
print("After context:")
print("Back to normal output")
print("""
WHY: The try/finally ensures cleanup happens even if an exception occurs
inside the with block. The decorator handles all the __enter__/__exit__
machinery for you.
""")
# ============ EXAMPLE 4: Yielding None ============
print("\n# Example 4: Yielding None (No Return Value)")
print("=" * 70)
@contextlib.contextmanager
def timed_operation(operation_name):
"""
Context manager that measures execution time.
Sometimes you don't yield a value - you just yield None or nothing.
The context manager still works; 'as variable' will be None.
"""
import time
start_time = time.time()
print(f" Starting {operation_name}")
try:
yield # No value returned
finally:
elapsed = time.time() - start_time
print(f" Finished {operation_name} in {elapsed:.4f} seconds")
print("Measuring operation time:")
with timed_operation("file read"):
# Simulate work
data = sum(range(1000000))
print()
print("Another operation:")
with timed_operation("file write"):
# Simulate work
temp = [x for x in range(100000)]
print("""
WHY: Not all context managers need to return a value. Sometimes they just
manage state or timing. Yielding None (or nothing) is perfectly valid.
""")
# ============ EXAMPLE 5: Exception Handling ============
print("\n# Example 5: Handling Exceptions in Decorated Generators")
print("=" * 70)
@contextlib.contextmanager
def error_handler(exception_type):
"""
A decorator-based context manager that can suppress exceptions.
The key insight: exceptions raised in the with block propagate to
the code after yield. You can catch them with try/except.
"""
print(f" Entering (will handle {exception_type.__name__})")
try:
yield
except exception_type as e:
print(f" Caught and suppressed: {exception_type.__name__}")
# Exception is suppressed by not re-raising
except Exception as e:
print(f" Caught but re-raising: {type(e).__name__}")
raise
finally:
print(f" Always cleaning up")
print("Suppressing ValueError:")
with error_handler(ValueError):
print(" Doing work...")
raise ValueError("Something went wrong")
print(" After with block (exception was suppressed)")
print()
print("Not suppressing TypeError:")
try:
with error_handler(ValueError):
print(" Doing work...")
raise TypeError("Wrong type!")
except TypeError:
print(" After with block (exception propagated)")
print("""
WHY: Decorated generators let you handle exceptions elegantly.
- Don't re-raise to suppress
- Re-raise to propagate
- finally block always runs (cleanup guaranteed)
""")
# ============ EXAMPLE 6: Resource Management Pattern ============
print("\n# Example 6: Database-Like Resource Management")
print("=" * 70)
class FakeDatabase:
"""Simulates a database connection."""
def __init__(self, name):
self.name = name
self.is_open = False
def open(self):
self.is_open = True
print(f" Database '{self.name}' opened")
def close(self):
self.is_open = False
print(f" Database '{self.name}' closed")
def query(self, sql):
if not self.is_open:
raise RuntimeError("Database is closed")
return f"Results of: {sql}"
@contextlib.contextmanager
def database_session(db_name):
"""
Context manager for database operations.
Pattern: open connection, yield it, close on exit.
This is how many real database libraries work.
"""
db = FakeDatabase(db_name)
db.open()
try:
yield db
finally:
db.close()
print("Using database context manager:")
with database_session("mydb") as db:
print(f" Database open: {db.is_open}")
result = db.query("SELECT * FROM users")
print(f" Query result: {result}")
print(f" After with block: {db.is_open}")
print()
print("Exception handling in database context:")
try:
with database_session("tempdb") as db:
print(f" Database open: {db.is_open}")
# Simulate error
raise ValueError("Invalid query")
except ValueError:
print(" Exception propagated and caught")
print(f" Database closed during exception: {db.is_open}")
print("""
WHY: Real libraries (SQLAlchemy, psycopg2, etc.) use this pattern.
The decorator makes it simple to write context managers that acquire
and release resources safely.
""")
# ============ EXAMPLE 7: Complex Setup and Teardown ============
print("\n# Example 7: Multi-Step Setup and Cleanup")
print("=" * 70)
@contextlib.contextmanager
def multi_step_context():
"""
Context manager with multiple setup/teardown steps.
Even complex operations stay clean with the decorator.
"""
print(" Step 1: Initialize resources")
resource1 = "resource_1"
print(" Step 2: Configure resource")
resource2 = "resource_2"
print(" Step 3: Open connections")
print(" SETUP COMPLETE")
try:
yield (resource1, resource2)
finally:
print(" Step 1: Close connections")
print(" Step 2: Save state")
print(" Step 3: Release resources")
print(" CLEANUP COMPLETE")
print("Multi-step context manager:")
with multi_step_context() as (r1, r2):
print(f" Working with {r1} and {r2}")
print("""
WHY: Complex initialization and cleanup is still readable with decorators.
The setup code is grouped at the top, cleanup at the bottom (in finally).
""")
# ============ EXAMPLE 8: Nesting and Composition ============
print("\n# Example 8: Combining Multiple Context Managers")
print("=" * 70)
@contextlib.contextmanager
def lock_context(name):
"""Simulates acquiring and releasing a lock."""
print(f" Acquired lock: {name}")
try:
yield
finally:
print(f" Released lock: {name}")
@contextlib.contextmanager
def transaction_context():
"""Simulates a database transaction."""
print(" BEGIN TRANSACTION")
try:
yield
finally:
print(" COMMIT TRANSACTION")
print("Combining context managers with nesting:")
with lock_context("data_lock"):
with transaction_context():
print(" Executing operations under lock and transaction")
print()
print("Cleaner syntax with multiple contexts (Python 3.10+):")
print("""
with lock_context("lock"), transaction_context():
# operations here
""")
print("""
WHY: Context managers compose well. You can nest them when you need
multiple resources, or use comma syntax for cleaner code.
""")
# ============ EXAMPLE 9: When to Use Decorator vs Class ============
print("\n# Example 9: Decorator vs Class - When to Use Each")
print("=" * 70)
print("""
USE @contextmanager (DECORATOR) WHEN:
✓ Simple setup/cleanup pattern
✓ Just need to yield a value once
✓ Logic is straightforward
✓ Less than ~50 lines of code
EXAMPLE: File cleanup, timing, simple redirects, database transactions
@contextlib.contextmanager
def simple():
setup()
try:
yield value
finally:
cleanup()
USE A CLASS WHEN:
✓ Need complex initialization
✓ Need multiple methods
✓ State management is complex
✓ Need to support multiple protocols (__enter__, __exit__, and others)
✓ Plan to reuse in multiple contexts
EXAMPLE: Database connection pool, transaction with savepoints, API wrapper
class ComplexManager:
def __init__(self, ...):
# Complex init
def __enter__(self):
# Setup
def __exit__(self, ...):
# Cleanup
def helper_method(self):
# Additional methods
RULE OF THUMB: Start with @contextmanager. If you find yourself adding
complexity, convert to a class. The decorator is for simple cases.
""")
# ============ EXAMPLE 10: Real-World Example - Temporary Directory ============
print("\n# Example 10: Real-World Example - Temporary File Management")
print("=" * 70)
import tempfile
import os
@contextlib.contextmanager
def temporary_directory():
"""
Context manager for temporary directory.
This shows a realistic use case: manage a temporary resource
that needs cleanup.
"""
tmpdir = tempfile.mkdtemp()
print(f" Created temporary directory: {tmpdir}")
try:
yield tmpdir
finally:
# Clean up
if os.path.exists(tmpdir):
print(f" Removing temporary directory: {tmpdir}")
# In real code: shutil.rmtree(tmpdir)
# Here we just print it
print("Using temporary directory:")
with temporary_directory() as tmpdir:
print(f" Working in {tmpdir}")
# Create files, do work, etc.
print(f" Directory exists: {os.path.exists(tmpdir)}")
print(f" Directory cleaned up after with block")
print("""
WHY: Real libraries (tempfile, sqlite3, requests, etc.) use decorated
context managers extensively. This pattern is idiomatic Python.
REAL-WORLD USAGE:
with tempfile.TemporaryDirectory() as tmpdir:
# Create files, do work
# tmpdir automatically cleaned up
with requests.Session() as session:
response = session.get(url)
# Connection automatically closed
with open('file.txt') as f:
data = f.read()
# File automatically closed
""")
# ============ EXAMPLE 11: Summary and Comparison ============
print("\n# Example 11: Summary - Class vs Decorator")
print("=" * 70)
print("""
CONTEXT MANAGER AS CLASS:
class ManagedResource:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f"Opening {self.name}")
return self
def __exit__(self, exc_type, exc_value, traceback):
print(f"Closing {self.name}")
return False
with ManagedResource("file") as res:
# use res
SAME CONTEXT MANAGER WITH DECORATOR:
@contextlib.contextmanager
def managed_resource(name):
print(f"Opening {name}")
try:
yield type('Resource', (), {'name': name})()
finally:
print(f"Closing {name}")
with managed_resource("file") as res:
# use res
The decorator is simpler for straightforward cases. The class is better
for complex logic.
""")
print("\n" + "=" * 70)
print("KEY TAKEAWAYS")
print("=" * 70)
print("""
1. @contextmanager TURNS GENERATORS INTO CONTEXT MANAGERS: Decorate a
generator function and it becomes usable with 'with' statements.
2. THE YIELD PATTERN IS CLEAR: Code before yield = __enter__,
code after yield = __exit__. Very readable!
3. try/finally ENSURES CLEANUP: Always use try/finally around yield
to guarantee cleanup happens, even on exceptions.
4. DECORATOR SIMPLIFIES COMMON CASE: For simple setup/cleanup patterns,
the decorator is far less verbose than a class.
5. EXCEPTION HANDLING WORKS NATURALLY: Exceptions in the with block
propagate to the code after yield. Catch them with try/except.
6. YIELD NOTHING FOR SIDE-EFFECT MANAGERS: Not all context managers
return values. Some just manage state (timing, locks, transactions).
7. CHOOSE BASED ON COMPLEXITY: Decorator for simple cases, class for
complex cases. Start with the decorator, convert to class if needed.
8. STANDARD LIBRARIES USE THIS: Many real libraries use @contextmanager.
Understanding it helps you read and use library code confidently.
NEXT: Explore advanced patterns like ExitStack for managing multiple
contexts dynamically, or write your own context managers for domain
-specific resources (database transactions, API sessions, etc.).
""")
```
Exercises¶
Exercise 1.
Create a Timer context manager that measures elapsed time. __enter__ records the start time and returns self. __exit__ computes the elapsed time and stores it in self.elapsed. Demonstrate with a with block that sleeps briefly, then print the elapsed time.
Solution to Exercise 1
import time
class Timer:
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.time() - self.start
return False
with Timer() as t:
time.sleep(0.1)
print(f"Elapsed: {t.elapsed:.3f}s") # ~0.100s
Exercise 2.
Write an Indenter context manager that tracks indentation level. Each nested with Indenter() block increases the indent level. Provide a print(text) method that prints text with the current indentation. Show nested usage producing indented output.
Solution to Exercise 2
class Indenter:
_level = 0
def __enter__(self):
Indenter._level += 1
return self
def __exit__(self, exc_type, exc_val, exc_tb):
Indenter._level -= 1
return False
def print(self, text):
print(" " * Indenter._level + text)
with Indenter() as i1:
i1.print("Level 1")
with Indenter() as i2:
i2.print("Level 2")
with Indenter() as i3:
i3.print("Level 3")
i1.print("Back to level 1")
# Level 1
# Level 2
# Level 3
# Back to level 1
Exercise 3.
Build a Transaction context manager for a simple in-memory database (a dictionary). __enter__ saves a snapshot of the data. If the block completes without exception, changes are kept. If an exception occurs, __exit__ rolls back to the snapshot. Demonstrate both successful and rolled-back transactions.
Solution to Exercise 3
class Transaction:
def __init__(self, database):
self.database = database
def __enter__(self):
self._snapshot = dict(self.database)
return self.database
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
self.database.clear()
self.database.update(self._snapshot)
print("Transaction rolled back")
return True # Suppress exception
print("Transaction committed")
return False
db = {"name": "Alice", "balance": 100}
# Successful transaction
with Transaction(db) as data:
data["balance"] = 200
print(db) # {'name': 'Alice', 'balance': 200}
# Failed transaction
with Transaction(db) as data:
data["balance"] = 9999
raise ValueError("Something went wrong")
print(db) # {'name': 'Alice', 'balance': 200} — rolled back
Exercise 4.
Explain why the following context manager is dangerous. What happens if the with block raises a ValueError? Rewrite __exit__ so that it only suppresses ZeroDivisionError and lets all other exceptions propagate.
```python class SuppressAll: def enter(self): return self
def __exit__(self, exc_type, exc_val, exc_tb):
return True # Suppresses ALL exceptions!
```
Solution to Exercise 4
return True in __exit__ suppresses every exception — ValueError, TypeError, KeyboardInterrupt, SystemExit, everything. This hides bugs silently: the program continues as if nothing went wrong, but the expected operation never completed. Debugging becomes nearly impossible because errors leave no trace.
If the with block raises ValueError, the error is swallowed and execution continues after the with block — the caller has no idea something failed.
Fixed version:
class SuppressZeroDivision:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ZeroDivisionError:
print("Caught ZeroDivisionError, suppressing")
return True # Suppress only this one
return False # Propagate everything else
with SuppressZeroDivision():
x = 1 / 0 # Suppressed
with SuppressZeroDivision():
x = int("abc") # ValueError propagates — not suppressed
Rule of thumb: only suppress exceptions you explicitly expect and can handle. For everything else, let them propagate. The stdlib provides contextlib.suppress(ExceptionType) for this exact use case.
Exercise 5.
Build a Redirect context manager that temporarily redirects sys.stdout to a StringIO buffer. Inside the with block, all print() output goes to the buffer instead of the console. After the block, stdout is restored and the captured output is available as self.output. Demonstrate by capturing printed output and verifying it as a string.
Solution to Exercise 5
import sys
from io import StringIO
class Redirect:
def __enter__(self):
self._original = sys.stdout
self._buffer = StringIO()
sys.stdout = self._buffer
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.output = self._buffer.getvalue()
sys.stdout = self._original
self._buffer.close()
return False
with Redirect() as r:
print("Hello, captured world!")
print("Line two")
# stdout is restored — this prints normally
print(f"Captured: {r.output!r}")
# Captured: 'Hello, captured world!\nLine two\n'
assert "Hello" in r.output
assert r.output.count("\n") == 2
This pattern is used in testing frameworks to capture printed output for assertions. The stdlib provides contextlib.redirect_stdout for the same purpose, but building it from scratch demonstrates how __enter__ (save + replace) and __exit__ (restore) work together to manage temporary state changes safely.
Exercise 6.
Consider the following function. A student worries that return inside a with block might skip f.close(), causing a resource leak. Determine the exact order of operations when return executes inside a with block. Does the file get closed? Why?
python
def load_file(filename):
with open(filename, "r") as f:
return f.read()
Solution to Exercise 6
The file is always closed properly. The execution order is:
text
1. Evaluate f.read()
2. Prepare the return value
3. Exit the with-block
4. Call f.__exit__() → this calls f.close()
5. Return from the function
6. Destroy the function frame
The with statement is syntactic sugar for a try/finally block:
python
f = open(filename, "r")
try:
result = f.read()
return result
finally:
f.close() # always runs, even on return
Python guarantees that __exit__ (and therefore finally) runs before the function actually returns, even if return appears inside the with block. The return value is saved, cleanup runs, and then the saved value is returned. There is no resource leak.