Free — no signup required

Context Managers: The 'with' Statement

3 min read

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.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free