Difficulty: Advanced
Context managers in Python provide a clean way to manage resources that need setup and teardown, such as database connections, locks, or temporary state changes. The `with` statement ensures that cleanup code always runs, even if an exception occurs inside the block.
The context manager protocol requires two methods: __enter__() and __exit__(). When a `with` block begins, Python calls __enter__() on the context manager, and the return value is bound to the variable after `as`. When the block ends (normally or due to an exception), Python calls __exit__(), which receives exception information as arguments. If __exit__ returns True, the exception is suppressed; otherwise it propagates.
For simpler cases, the contextlib module provides the @contextmanager decorator, which lets you write a context manager as a generator function with a single yield. Code before yield runs as __enter__, the yielded value is bound to the `as` variable, and code after yield runs as __exit__. This avoids the boilerplate of writing a full class.
Context managers are used throughout Python's standard library and popular frameworks. The open() function is the most common example, but they also appear in threading (locks), decimal (local precision), and database libraries (transactions). Understanding context managers is essential for writing robust, resource-safe Python code.
You can also nest context managers or use contextlib.ExitStack to manage a dynamic number of context managers. These advanced patterns are useful when the number of resources to manage is not known until runtime.
class ManagedResource:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f"Acquiring {self.name}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Releasing {self.name}")
return False
with ManagedResource("database") as res:
print(f"Using {res.name}")
__enter__ is called when entering the with block and returns self (bound to 'res'). __exit__ is called when leaving the block, ensuring cleanup always happens.
class SafeBlock:
def __enter__(self):
print("Entering safe block")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
print(f"Caught exception: {exc_val}")
print("Exiting safe block")
return True
with SafeBlock():
print("Inside block")
raise ValueError("something went wrong")
print("After block")
Returning True from __exit__ suppresses the exception. Without return True, the ValueError would propagate after __exit__ runs.
from contextlib import contextmanager
@contextmanager
def tag(name):
print(f"<{name}>")
yield name
print(f"</{name}>")
with tag("div") as t:
print(f"Content inside {t}")
The @contextmanager decorator turns a generator into a context manager. Code before yield is __enter__, the yielded value is the 'as' variable, and code after yield is __exit__.
class Indent:
level = 0
def __init__(self, label):
self.label = label
def __enter__(self):
Indent.level += 1
print(" " * Indent.level + f"Enter {self.label}")
return self
def __exit__(self, *args):
print(" " * Indent.level + f"Exit {self.label}")
Indent.level -= 1
return False
with Indent("outer"):
with Indent("inner"):
print(" " * Indent.level + "Deep inside")
Each nested with block calls __enter__ on entry and __exit__ on exit. The class-level 'level' variable tracks nesting depth for proper indentation.
with statement, __enter__, __exit__, contextlib.contextmanager