pyguides

with

The with statement in Python simplifies resource management by handling setup and cleanup automatically. It ensures that resources like files, database connections, or locks are properly acquired and released, even if errors occur during execution.

Syntax

with expression as variable:
    # Block where variable is available

The expression must return a context manager — an object that implements __enter__ and __exit__ methods. These two methods form the protocol that every context manager must satisfy, and understanding the protocol helps you use both built-in and custom context managers correctly.

Basic examples

Opening a file

The most common use case is file handling, where the with statement guarantees that the file descriptor is released when the block completes:

with open("example.txt", "w") as f:
    f.write("Hello, World!")
# File is automatically closed when exiting the block

Writing to a file demonstrates the acquisition side of the with statement, but reading is equally common and shows the same cleanup guarantee in a different direction. The following example reads back what was just written, illustrating that the automatic close happens regardless of whether you are consuming data from the file or producing it.

Reading a file

with open("example.txt", "r") as f:
    content = f.read()
    print(content)
# File is automatically closed after reading

The file is guaranteed to be closed, even if an error occurs while reading. This guarantee is the fundamental promise of context managers — cleanup happens no matter how the block exits, whether normally or through an exception. Understanding how this mechanism works under the hood reveals why the with statement is more than just syntactic sugar for try-finally blocks.

How it works

Under the hood

When you use with, Python calls:

  1. __enter__() — runs at the start of the block, returns the value bound to as
  2. Your code executes inside the block
  3. __exit__() — runs when exiting the block, even if an exception occurred
class MyContext:
    def __enter__(self):
        print("Entering context")
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Exiting context")
        return False

with MyContext() as ctx:
    print("Inside block")
# Entering context
# Inside block
# Exiting context

This custom context manager reveals the enter-exit lifecycle explicitly, printing at each phase so you can observe the order of execution. The enter method runs first and its return value becomes the variable bound by the as clause. The exit method follows after the block completes, receiving exception details that allow the context manager to decide whether to suppress or propagate any error that occurred during execution.

The as clause

The as clause binds the result of __enter__() to a variable:

with open("file.txt") as f:
    # f is the file object returned by open().__enter__()
    pass

You can skip the as clause if you don’t need the bound value. This is useful when the context manager performs a side effect — like acquiring a lock or changing a global setting — and you do not need to interact with the manager object itself within the block body.

with open("file.txt"):
    # File is open, but no variable bound
    pass

Skipping the as clause works for any context manager where the side effect of entering the context is the primary goal, such as temporarily redirecting output or suppressing exceptions. When you do need to manage multiple resources simultaneously, the with statement supports stacking contexts in several ways, each with different readability tradeoffs.

Multiple context managers

Stacking multiple with statements

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

Nesting with statements is the most explicit approach — each resource gets its own block and its own level of indentation, making it clear which operations depend on which resource. This style works in all Python versions and makes the dependency hierarchy visible at a glance, though deeply nested contexts can become hard to follow if you manage more than a few resources.

Python 3.1+ — Multiple context managers

Python 3.1 introduced a more concise syntax that combines multiple context managers on a single line separated by commas:

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

This single-line form eliminates the nesting that the previous example required, making the code flatter and easier to scan when both resources are at the same logical level of importance. The comma-separated syntax is particularly useful when the resources are independent of each other and none depends on the result of entering another context for its own setup.

Python 3.10+ — Parenthesized context managers

Python 3.10 allows line breaks by wrapping the context manager list in parentheses, making it practical to manage several resources without extreme horizontal scrolling:

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

The parenthesized form combines the flat structure of the comma-separated syntax with the readability of the nested approach, giving each resource its own visual line while keeping them all at the same indentation level. This is the preferred style in modern Python codebases when working with three or more context managers that need to be open simultaneously.

Exception handling

The __exit__ method receives exception information including the exception type, value, and traceback, giving the context manager complete visibility into any error that occurred within the block:

def __exit__(self, exc_type, exc_val, exc_tb):
    if exc_type is not None:
        print(f"Exception occurred: {exc_val}")
    # Return True to suppress the exception
    # Return False (or None) to propagate it
    return False

Receiving exception information is powerful, but returning True from exit takes the pattern a step further by suppressing the exception entirely. This capability enables context managers that act as error boundaries, swallowing specific exception types while letting others propagate normally. The following example demonstrates a context manager that suppresses every exception unconditionally.

Suppressing exceptions

Return True from __exit__ to suppress an exception:

class SuppressError:
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        return True  # Suppress all exceptions

with SuppressError():
    raise ValueError("This will be suppressed")
# No error raised

While this example suppresses everything for demonstration purposes, real-world usage typically inspects the exception type and only suppresses specific errors. Python’s standard library includes contextlib.suppress for exactly this purpose, providing a ready-made context manager that swallows named exception types without requiring you to write a custom class. Several other built-in context managers ship with Python and cover common patterns.

Built-in context managers

open()

Files are context managers that automatically close when the block exits:

with open("file.txt") as f:
    data = f.read()

File handling is the most familiar context manager, but the same enter-exit pattern applies to many other standard library objects. Thread synchronization primitives use context management to guarantee that locks are released even when the protected code raises an exception, preventing the deadlocks that would occur if a thread acquired a lock and then crashed before releasing it.

threading.Lock()

Locks for thread synchronization:

import threading

lock = threading.Lock()

with lock:
    # Only one thread can enter this block
    critical_section()

Lock context management ensures that no thread can hold a lock indefinitely due to an unexpected exception. Beyond thread safety, context managers also handle temporary state changes, such as modifying global interpreter settings for the duration of a computation. The decimal module provides a context manager that temporarily adjusts numeric precision without permanently altering the global configuration.

decimal.localcontext()

Temporarily change decimal precision:

from decimal import Decimal, localcontext

with localcontext() as ctx:
    ctx.prec = 2
    result = Decimal("1.234") * Decimal("5.678")
    print(result)  # 7.0

The localcontext manager restores the original precision setting when the block exits, making it safe to perform high-precision or low-precision calculations in isolated sections of code without affecting other parts of the program. Similarly, redirecting standard output is another temporary state change that benefits from context management, allowing you to capture printed output without modifying every print call in your code.

contextlib.redirect_stdout()

Temporarily redirect standard output:

from contextlib import redirect_stdout
import io

buffer = io.StringIO()

with redirect_stdout(buffer):
    print("This goes to the buffer")

print(buffer.getvalue())

Output redirection through a context manager is especially useful in testing, where you need to capture what a function prints without changing its implementation. After seeing the built-in context managers that Python provides, the next step is creating your own custom managers for domain-specific resource management patterns.

Creating custom context managers

Using a class

class Timer:
    def __enter__(self):
        self.start = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.elapsed = time.time() - self.start
        print(f"Elapsed: {self.elapsed:.2f}s")

with Timer():
    time.sleep(1)
# Elapsed: 1.00s

The class-based approach to custom context managers gives you full control over enter and exit logic, but simpler cases benefit from a decorator-based alternative that reduces boilerplate. The contextlib.contextmanager decorator lets you write a generator function where the code before yield runs in enter and the code after yield runs in exit, eliminating the need to define a class with two methods.

Using contextlib.contextmanager

A decorator-based approach:

from contextlib import contextmanager
import time

@contextmanager
def timer():
    start = time.time()
    try:
        yield
    finally:
        elapsed = time.time() - start
        print(f"Elapsed: {elapsed:.2f}s")

with timer():
    time.sleep(0.5)
# Elapsed: 0.50s

The generator-based approach is usually preferred for simple context managers because it is more concise and the try-finally structure makes the cleanup logic explicit. For more complex scenarios involving asynchronous code, Python provides async context managers that follow the same pattern but use async with and async generator functions, enabling context-managed resource handling in coroutines and async event loops.

Using contextlib.AsyncExitStack

For async context managers:

from contextlib import asynccontextmanager

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

Async context managers follow the same enter-yield-exit pattern as their synchronous counterparts, but they integrate with the async event loop so that entering and exiting the context can await other coroutines. This pattern is essential for managing database connections, HTTP sessions, and other asynchronous resources in modern Python applications that use asyncio or similar frameworks.

Common patterns

Database connections

import sqlite3

with sqlite3.connect("database.db") as conn:
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users")
    results = cursor.fetchall()
# Connection automatically closed

Database connections are among the most critical resources to manage with context managers because an unclosed connection can exhaust the connection pool and block other parts of the application. The automatic commit or rollback behavior that some database drivers implement in their exit methods adds another layer of safety beyond simple connection cleanup.

Temporary files

import tempfile

with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp:
    tmp.write("Temporary content")
    tmp_name = tmp.name
# File closed, but not deleted (delete=False)

Temporary files demonstrate a context manager where the cleanup behavior is configurable through the context manager’s constructor parameters. The delete flag controls whether the file is removed from disk when the context exits, giving you the option to keep temporary files for inspection during debugging while still guaranteeing that the file handle itself is closed.

Lock management

import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    with lock:
        counter += 1
        return counter

Lock management with context managers is the canonical pattern for thread-safe counter increments and other atomic operations in multithreaded Python code. The with statement ensures that the lock is released even if the increment function raises an exception, preventing the deadlock scenarios that manual lock acquisition and release would risk.

Best practices

Always use with for resources

# Bad — file may not close on error
f = open("file.txt")
f.write("data")
f.close()

# Good — guaranteed cleanup
with open("file.txt", "w") as f:
    f.write("data")

Manual resource management puts the burden of remembering to call close on the programmer, and any exception between open and close leaves the resource leaked. The with statement eliminates this entire class of bugs by making cleanup automatic, which is why code review tools and linters flag bare open calls without corresponding close statements in a finally block or with statement.

Avoid long operations in with block

Keep the block short to minimize resource holding time:

# Good — file closed quickly
with open("large_file.txt") as f:
    data = f.read()
process_data(data)  # Outside the with block

Holding resources for longer than necessary degrades performance and can cause contention when other parts of the program or other processes need access to the same resource. Moving expensive processing outside the with block keeps resource acquisition windows short and reduces the likelihood of timeouts and lock contention in concurrent environments.

Nest only when necessary

Deeply nested context managers can be hard to read:

# Hard to read
with open("a") as a, open("b") as b, open("c") as c:
    pass

# Easier to read
with open("a") as a:
    with open("b") as b:
        with open("c") as c:
            pass

The tradeoff between flat and nested context manager syntax depends on the logical relationship between the resources. When resource B depends on resource A being available, nesting reflects the dependency hierarchy accurately. When all resources are independent, the flat comma-separated or parenthesized form is clearer. Choose the structure that matches the actual dependency graph of your resources rather than defaulting to one style universally.

See Also