Context managers handle setup and cleanup automatically. Here's how they work.

The Basics

# Without context manager
file = open("data.txt")
try:
    content = file.read()
finally:
    file.close()
 
# With context manager
with open("data.txt") as file:
    content = file.read()
# File automatically closed

The with statement guarantees cleanup, even if exceptions occur.

How It Works

Context managers implement two methods:

class MyContext:
    def __enter__(self):
        # Setup - runs at start of with block
        print("Entering")
        return self  # Value bound to 'as' variable
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        # Cleanup - runs at end of with block
        print("Exiting")
        # Return True to suppress exceptions
        return False
 
with MyContext() as ctx:
    print("Inside")
 
# Output:
# Entering
# Inside
# Exiting

Common Use Cases

File handling

with open("data.txt", "w") as f:
    f.write("Hello")
# File closed automatically

Database connections

with get_connection() as conn:
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users")
# Connection closed/returned to pool

Locks

import threading
 
lock = threading.Lock()
with lock:
    # Critical section
    shared_resource.update()
# Lock released automatically

Temporary changes

import os
 
@contextmanager
def change_dir(path):
    old_dir = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(old_dir)
 
with change_dir("/tmp"):
    # Working in /tmp
    pass
# Back to original directory

The contextlib Module

@contextmanager decorator

The easy way to create context managers:

from contextlib import contextmanager
 
@contextmanager
def timer():
    start = time.time()
    yield
    elapsed = time.time() - start
    print(f"Elapsed: {elapsed:.2f}s")
 
with timer():
    do_something()
# Prints elapsed time

suppress exceptions

from contextlib import suppress
 
with suppress(FileNotFoundError):
    os.remove("maybe_exists.txt")
# No error if file doesn't exist

redirect output

from contextlib import redirect_stdout
import io
 
buffer = io.StringIO()
with redirect_stdout(buffer):
    print("Captured!")
output = buffer.getvalue()  # "Captured!\n"

nullcontext

from contextlib import nullcontext
 
# Useful for optional context managers
cm = open(file) if file else nullcontext()
with cm:
    process()

Multiple Context Managers

# Nested
with open("input.txt") as infile:
    with open("output.txt", "w") as outfile:
        outfile.write(infile.read())
 
# Same line (Python 3.10+)
with (
    open("input.txt") as infile,
    open("output.txt", "w") as outfile,
):
    outfile.write(infile.read())

Exception Handling

class Transaction:
    def __enter__(self):
        self.start_transaction()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            # Exception occurred - rollback
            self.rollback()
        else:
            # Success - commit
            self.commit()
        return False  # Don't suppress exceptions
 
with Transaction() as tx:
    tx.execute("INSERT ...")
    tx.execute("UPDATE ...")
# Auto-commits or rollbacks

Async Context Managers

class AsyncDB:
    async def __aenter__(self):
        self.conn = await connect()
        return self
    
    async def __aexit__(self, *args):
        await self.conn.close()
 
async def main():
    async with AsyncDB() as db:
        await db.query("SELECT ...")

Or with decorator:

from contextlib import asynccontextmanager
 
@asynccontextmanager
async def get_session():
    session = await create_session()
    try:
        yield session
    finally:
        await session.close()

My Patterns

  1. Use context managers for any resource that needs cleanup
  2. Prefer @contextmanager for simple cases
  3. Return useful values from __enter__
  4. Don't suppress exceptions unless intentional
  5. Write async versions for async resources

Context managers make resource handling bulletproof. Use them everywhere cleanup matters.

React to this post: