Context managers handle setup and teardown automatically. Here's everything you need to know.

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 is automatically closed

The with statement ensures cleanup happens even if an exception occurs.

Common Use Cases

Files:

with open("output.txt", "w") as f:
    f.write("Hello, World!")

Database connections:

with sqlite3.connect("database.db") as conn:
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users")

Locks:

import threading
 
lock = threading.Lock()
with lock:
    # Critical section
    shared_resource.modify()

HTTP sessions:

import requests
 
with requests.Session() as session:
    response = session.get("https://api.example.com")

How It Works

Context managers implement two methods:

class MyContext:
    def __enter__(self):
        # Setup code
        print("Entering context")
        return self  # Value bound to 'as' variable
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        # Cleanup code
        print("Exiting context")
        return False  # Don't suppress exceptions
 
with MyContext() as ctx:
    print("Inside context")
 
# Output:
# Entering context
# Inside context
# Exiting context

The __exit__ method receives exception info if one occurred:

  • exc_type: Exception class (or None)
  • exc_val: Exception instance (or None)
  • exc_tb: Traceback (or None)

Return True from __exit__ to suppress the exception.

Using contextlib

The contextlib module provides shortcuts.

@contextmanager decorator

Turn a generator into a context manager:

from contextlib import contextmanager
 
@contextmanager
def timer(name):
    import time
    start = time.time()
    try:
        yield  # Code inside 'with' block runs here
    finally:
        elapsed = time.time() - start
        print(f"{name} took {elapsed:.2f}s")
 
with timer("Processing"):
    # Do work
    process_data()

Everything before yield is __enter__, everything after is __exit__.

Passing values

@contextmanager
def temp_directory():
    import tempfile
    import shutil
    
    path = tempfile.mkdtemp()
    try:
        yield path  # Pass the path to the with block
    finally:
        shutil.rmtree(path)
 
with temp_directory() as tmpdir:
    # Use tmpdir
    save_file(f"{tmpdir}/data.txt")
# Directory is deleted after

suppress()

Ignore specific exceptions:

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

redirect_stdout/redirect_stderr

from contextlib import redirect_stdout
from io import StringIO
 
buffer = StringIO()
with redirect_stdout(buffer):
    print("This goes to buffer")
 
output = buffer.getvalue()

Nesting Context Managers

Multiple managers in one with:

with open("input.txt") as infile, open("output.txt", "w") as outfile:
    outfile.write(infile.read().upper())

Or use ExitStack for dynamic nesting:

from contextlib import ExitStack
 
with ExitStack() as stack:
    files = [
        stack.enter_context(open(f"file{i}.txt"))
        for i in range(10)
    ]
    # All files are open
# All files are closed

Real-World Example

A database transaction manager:

from contextlib import contextmanager
 
@contextmanager
def transaction(connection):
    cursor = connection.cursor()
    try:
        yield cursor
        connection.commit()
    except Exception:
        connection.rollback()
        raise
    finally:
        cursor.close()
 
# Usage
with transaction(db_conn) as cursor:
    cursor.execute("INSERT INTO users VALUES (?)", (name,))
    cursor.execute("UPDATE stats SET count = count + 1")
# Auto-commits on success, rolls back on error

Async Context Managers

For async code, use async with:

class AsyncResource:
    async def __aenter__(self):
        await self.connect()
        return self
    
    async def __aexit__(self, *args):
        await self.disconnect()
 
async with AsyncResource() as resource:
    await resource.do_work()

Or with @asynccontextmanager:

from contextlib import asynccontextmanager
 
@asynccontextmanager
async def async_timer(name):
    start = time.time()
    try:
        yield
    finally:
        print(f"{name}: {time.time() - start:.2f}s")

When to Use Context Managers

Use them when you have:

  • Resources that need cleanup (files, connections, locks)
  • Setup/teardown pairs
  • Temporary state changes
  • Transaction-like operations

They make code cleaner and prevent resource leaks. If you're writing try/finally for cleanup, consider a context manager instead.

React to this post: