A context manager is what powers Python's with statement — the pattern you've almost certainly used as with open("file.txt") as f:. Its job is to guarantee that setup and cleanup code both run, no matter what happens in between, including if an exception is raised. That makes it the right tool any time you're managing a resource that needs to be released afterward: files, network connections, database transactions, or locks.
Class-Based Context Manager (__enter__ / __exit__)
class FakeConnection:
def __enter__(self):
print("Opening connection...")
return self # this becomes the `as` variable
def __exit__(self, exc_type, exc_value, traceback):
print("Closing connection...")
return False # False means: don't suppress the exception
def query(self, sql):
print(f"Running query: {sql}")
with FakeConnection() as conn:
conn.query("SELECT * FROM users")
print("---")
try:
with FakeConnection() as conn:
conn.query("SELECT * FROM users")
raise ValueError("Something went wrong mid-query")
except ValueError as e:
print(f"Caught: {e}")
Output:
Opening connection...
Running query: SELECT * FROM users
Closing connection...
---
Opening connection...
Running query: SELECT * FROM users
Closing connection...
Caught: Something went wrong mid-query
Notice that "Closing connection..." prints even in the second block, where an exception was raised inside the with body — Python still calls __exit__ before letting the exception propagate. __enter__ runs at the start of the block and returns the value bound to the as variable; __exit__ always runs at the end, and receives the exception details (if any) so it can decide whether to suppress them.
The Shortcut: @contextmanager
from contextlib import contextmanager
@contextmanager
def fake_connection():
print("Opening connection...")
try:
yield "connection-object"
finally:
print("Closing connection...")
with fake_connection() as conn:
print(f"Using {conn}")
Output:
Opening connection...
Using connection-object
Closing connection...
@contextmanager turns a regular generator function into a context manager: everything before yield becomes __enter__, the yielded value becomes the as variable, and everything after yield (here, in the finally block) becomes __exit__. The try/finally is essential — it's what guarantees the cleanup code still runs if the code inside the with block raises. For simple cases this decorator-based version is usually less boilerplate than writing a full class with both dunder methods.
