Automatic Cleanup
Think of a context manager like a hotel key card system. When you check in, the card is activated (setup). When you check out — whether you leave normally or get carried out on a stretcher — the card is deactivated (teardown). The hotel guarantees cleanup regardless of how you leave.
In Ops, we constantly deal with resources that must be cleaned up: file handles, network sockets, database connections, thread locks, and temporary directories. If your script opens a file and then crashes before closing it, you leak that file descriptor. Do this enough times and the OS refuses to open new files. On a long-running daemon, this is fatal.
The with statement guarantees teardown runs no matter what — even if an exception is raised inside the block.
# The fragile way — easy to forget the finally block
f = open('/var/log/deploy.log', 'w')
try:
f.write('Deployment started\n')
do_something_that_might_crash() # If this raises, does f.close() run?
finally:
f.close() # You must remember this every single time
# The Pythonic way — cleanup is guaranteed by the protocol
with open('/var/log/deploy.log', 'w') as f:
f.write('Deployment started\n')
do_something_that_might_crash()
# f.close() is called automatically here, even if the above line raised an exception
How It Works Internally
A context manager is any object that implements two special methods:
__enter__(self) — Called when execution enters the with block. Its return value is bound to the as variable. This is where you acquire the resource.
__exit__(self, exc_type, exc_val, exc_tb) — Called when execution leaves the with block, for any reason. The three arguments describe any exception that occurred (None, None, None if no exception). If __exit__ returns True, the exception is suppressed. If it returns False or None, the exception propagates normally.
class DatabaseConnection:
def __enter__(self):
self.conn = connect_to_db() # Acquire resource
return self.conn # This becomes the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
self.conn.rollback() # Exception occurred — roll back
else:
self.conn.commit() # Success — commit
self.conn.close() # Always close
return False # Don't suppress the exception
with DatabaseConnection() as conn:
conn.execute("UPDATE servers SET status='active' WHERE id=1")
# commit() and close() are called automatically
The contextlib Shortcut
For simple cases, writing a full class is verbose. The contextlib.contextmanager decorator lets you write a context manager as a generator function, which is far more readable:
from contextlib import contextmanager
import tempfile
import os
@contextmanager
def temp_workspace():
"""Creates a temp directory, yields it, then deletes it."""
path = tempfile.mkdtemp()
try:
yield path # Everything before yield is __enter__
finally:
shutil.rmtree(path) # Everything after yield is __exit__
with temp_workspace() as workspace:
# Do work in the temp directory
open(f"{workspace}/config.yaml", 'w').write("key: value")
# Directory is deleted automatically, even if an exception occurred
Key Point: The __exit__ method receives exception information, which means context managers can implement transactional semantics — commit on success, rollback on failure. This pattern is used by every serious database library, lock implementation, and resource pool in Python.