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:
__enter__()— runs at the start of the block, returns the value bound toas- Your code executes inside the block
__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
- break keyword — exit loops early
- except keyword — exception handling
- open built-in — file opening function
- contextlib module — context manager utilities