Python Multiple With Statements: Combining Context Managers
Learn how to combine multiple with statements in Python, control cleanup order, handle exceptions, and choose between flat, nested, or ExitStack-based code.
Python's with statement makes paired resource acquisition and release easier. A context manager is an object that provides __enter__ and __exit__, and with calls those methods before and after a block. When you need more than one resource, the two common forms are a comma-separated statement and nested with blocks. The syntax matters because it controls cleanup order, exception behavior, and how long each resource is held.
Combining Context Managers in One Statement
Python 2.7 and 3.1 introduced a compact way to write several context managers in one with statement by separating them with commas:
with open('input.txt') as in_file, open('output.txt', 'w') as out_file: out_file.write(in_file.read())
This is equivalent to nesting the statements, but with less indentation. The managers are entered from left to right, and their __exit__ methods are called in reverse order. If the first manager's __enter__ succeeds but the second one raises, the first is still closed.
The comma-separated form is a good fit when the resources are independent and the block is short.
Cleanup Order and Dependencies
Cleanup order is deterministic: the last context manager entered is the first to exit. This matters when one resource depends on another. With a database connection and transaction, you usually want the transaction to finish cleaning up before the connection closes:
with db_connection() as conn, conn.transaction() as txn: txn.execute(...)
Because each context manager expression is evaluated and entered in sequence, the second expression can use the value bound by the first. The reverse-order cleanup means txn.__exit__ runs before conn.__exit__. If evaluating the transaction expression or calling its __enter__ fails, the connection already entered is still closed.
When to Use Nested with Statements
Nesting is still useful when the inner block needs the outer resource itself, not just a value obtained from it. Copying a file line by line, for example, keeps the input file open while the output file is open:
with open('input.txt') as src: with open('output.txt', 'w') as dst: for line in src: dst.write(line)
The indentation makes the dependency visible. Nesting also lets you conditionally acquire the inner resource by putting the inner with inside an if block, which is not possible with a single comma-separated statement. If the resources are independent, a flat statement usually reads better.
Exceptions and Cleanup Errors
When multiple context managers are combined, an exception in the body is passed to each __exit__ method in reverse order. If one of those __exit__ methods raises, the new exception replaces the body exception. This can mask the original problem.
with open('a.txt') as a, open('b.txt') as b: raise ValueError('body error')
If a.__exit__ raises while the ValueError is active, the ValueError is lost. Cleanup failures are uncommon for standard library context managers, but custom managers can behave differently. If a cleanup failure can occur, handle it explicitly in __exit__ or in the surrounding code; do not expect a try around the body to preserve the original exception when a later __exit__ raises.
Readability, Dynamic Resources, and Resource Lifetime
The flat form reduces indentation, but a line with more than two or three managers is dense:
with open('a') as a, open('b') as b, open('c') as c, open('d') as d: ...
If the number of resources is not known until runtime, use contextlib.ExitStack instead:
from contextlib import ExitStack with ExitStack() as stack: files = [stack.enter_context(open(name)) for name in filenames] # process files
ExitStack calls each registered context manager's __exit__ when the with block exits, even if an error occurs while entering them.
The flat form acquires every resource before the body starts. If the body only needs one resource at a time, consider opening resources closer to the point where they are used, or use nested blocks, so you do not hold file descriptors, locks, or connections longer than necessary. The __enter__/__exit__ call overhead is usually small next to real I/O, but it is still worth keeping long-lived resources scoped tightly.
Compatibility Notes
The comma-separated form has been available since Python 2.7 and 3.1. contextlib.ExitStack was added in Python 3.3, so Python 2.7 code cannot use it; nested with statements remain the compatible option. The reverse-order cleanup is guaranteed because the comma-separated form is equivalent to nesting; use that rule when reasoning about exceptions and cleanup behavior.