Nested context managers in Python appear whenever you need to manage more than one resource inside a single with block. You might open a source file and a destination file at the same time, acquire a lock and then a second lock, or begin a database transaction and also redirect logging output. Each resource requires its own context manager, and Python provides several syntaxes for nesting them. The comma-separated with statement is the everyday form, the parenthesized syntax handles long lines, and the ExitStack tool from Python context managers with contextlib handles the dynamic case where the number of resources depends on runtime data.
The earlier articles in this section covered how individual context managers work and how to create them. This article focuses on composition: how multiple context managers interact when nested, what order they enter and exit, and how to choose the right nesting syntax for each situation. Understanding these composition rules is important because getting the order wrong can cause resource leaks, deadlocks, or data corruption, while getting it right produces code that is both correct and readable.
The fundamental rule of nested context managers is that they exit in reverse order of entry. If you enter context manager A and then context manager B, B exits first, then A. This last-in-first-out ordering is consistent with how nested with blocks behave and with how Python manages nested resources in general. It ensures that resources are released in the correct dependency order: the resource that was acquired second is released first, which is almost always what you want because the second resource often depends on the first being available.
Comma-separated context managers
The simplest way to nest context managers is to list them in a single with statement, separated by commas. Each context manager expression is evaluated, its enter method is called, and its return value is available through an as clause. When the block ends, all exit methods are called in reverse order. Here is a common example that copies content from one file to another:
with open("source.txt") as src, open("dest.txt", "w") as dst:
for line in src:
dst.write(line)The source file opens first, then the destination file. The block reads lines from the source and writes them to the destination. When the block ends, the destination file closes first, then the source file. If an exception occurs during the copy, both files still close because each context manager's exit method runs in its own finally clause equivalent within the synthesized nested blocks.
This comma-separated form is equivalent to writing nested with statements explicitly. The following code produces identical behaviour but with deeper indentation:
with open("source.txt") as src:
with open("dest.txt", "w") as dst:
for line in src:
dst.write(line)The comma-separated form is preferred when all context managers are available at the start of the block because it is flatter and avoids unnecessary indentation. The nested form is necessary when an inner context manager depends on a value produced by an outer one, as the next section demonstrates.
When explicit nesting is required
The comma-separated with statement requires all context manager expressions to be evaluated before any of them are entered. This means you cannot use a value from one context manager's as clause to construct another context manager in the same comma-separated list. When an inner context manager depends on a value from an outer one, you must nest the with statements explicitly.
A common example is reading a configuration file to determine an output path, then opening that output path for writing. The output path is not known until the configuration file has been read, which requires the configuration file to be opened first. The nesting makes the dependency explicit:
with open("config.txt") as config:
output_path = config.readline().strip()
with open(output_path, "w") as output:
output.write("Processed successfully")The outer with opens the configuration file and reads the first line to get the output path. The inner with uses that path to open the output file. Both files close when their respective blocks end. This pattern is not expressible with a comma-separated with statement because the output path expression depends on a value that is only available after the first context manager has been entered.
Another example is acquiring multiple locks that must be acquired in a specific order to prevent deadlocks. If a function needs two locks and the standard deadlock-avoidance rule requires acquiring them in a particular sequence, nesting the with statements enforces that sequence:
with lock_a:
with lock_b:
perform_critical_operation()Lock A is acquired first, then lock B. Lock B is released first, then lock A. The nesting enforces the acquisition order and makes it visible in the code structure. Anyone reading the code can see that lock A must be held to acquire lock B.
Parenthesized multi-line syntax
When a with statement manages many context managers, the comma-separated form can become too long for a single line. Python 3.10 introduced parenthesized context manager syntax, which allows the list of context managers to span multiple lines inside parentheses. This syntax is purely cosmetic (it produces the same behaviour as the comma-separated form) but significantly improves readability for blocks with three or more context managers:
with (
open("input.csv") as infile,
open("output.csv", "w") as outfile,
open("errors.log", "w") as errfile,
):
for line in infile:
try:
processed = transform(line)
outfile.write(processed)
except ValueError as e:
errfile.write(f"Error on line: {e}\n")The parenthesized form groups the context managers visually and allows each to appear on its own line with a trailing comma. The block reads from an input file, writes transformed data to an output file, and logs errors to a separate file. All three files are opened before the block runs and closed when the block ends, in reverse order. The syntax makes it clear that three resources are being managed without requiring horizontal scrolling.
The parenthesized form also supports as clauses on each context manager and trailing commas after the last one. The trailing comma is optional but recommended because it makes adding or reordering context managers easier during code review. The syntax works with any context manager, including custom ones you have written using classes or the contextmanager decorator.
Exceptions in nested context managers
When an exception occurs inside a nested with block, Python unwinds the context managers in reverse order, calling each one's exit method with the exception information. This unwinding continues even if an exit method itself raises an exception. If one exit method raises a new exception while handling an original exception, Python chains the exceptions together, and both are available in the traceback.
Consider a scenario where three nested context managers are active and an exception occurs in the block. Python calls the exit method of the innermost context manager first, passing the exception information. If that exit method returns False (propagating the exception), Python calls the exit method of the middle context manager with the same exception information. This continues until all context managers have been exited or one of them suppresses the exception by returning True.
If an exit method raises a new exception while processing the original one, Python does not abandon the remaining context managers. It records the new exception, continues calling exit on the remaining context managers, and then raises the chain of exceptions. This design ensures that all resources are cleaned up even when cleanup itself encounters errors. A file that cannot be closed because the disk is full still triggers an attempt to close the lock that was acquired after it, and the network connection that was opened last is still closed.
This resilience makes nested context managers suitable for managing resources in error-prone environments. Even when multiple things go wrong simultaneously, the with statement guarantees that every resource gets a chance to clean up. Understanding this behaviour helps you design context managers whose exit methods are as robust as possible, handling their own errors gracefully without preventing other context managers from running their cleanup.
The next article covers common Python decorator and context manager mistakes, cataloguing the errors that developers encounter most frequently when working with both abstractions and showing how to avoid them.
Rune AI
Key Insights
- Multiple context managers in a single with statement, separated by commas, are entered left-to-right and exited right-to-left.
- The parenthesized with syntax (Python 3.10+) lets you split long context manager lists across multiple lines for readability.
- ExitStack is the right tool when the number of context managers is determined at runtime, not compile time.
- Nested context managers compose naturally: each manager's exit runs regardless of whether earlier exits raised exceptions.
- Context managers and decorators compose as well, with decorators wrapping the function definition and context managers wrapping a block of code inside it.
Frequently Asked Questions
How do you nest multiple context managers in a single with statement?
What is the order of entry and exit for nested context managers?
Conclusion
Nesting context managers is how you manage multiple resources in a single with block, and Python provides several syntaxes for it. Comma-separated context managers are the everyday form, the parenthesized syntax handles long lines gracefully, and ExitStack handles the dynamic case where the number of resources is not known until runtime. Understanding how context managers compose and exit in reverse order helps you design resource hierarchies that are always cleaned up correctly.
More in this topic
Create Custom Iterators in Python
Learn how to build your own iterators in Python by implementing the __iter__() and __next__() methods, with practical examples and reusable patterns.
Python Iterator Protocol Explained
Learn how Python's iterator protocol works with the __iter__() and __next__() methods, and understand the contract every iterator must follow.
Python Iterables and Iterators Explained
Learn what iterables and iterators are in Python, how they power for loops under the hood, and why understanding them unlocks cleaner data processing patterns.