contextlib.ExitStack is a standard-library utility for managing a dynamic collection of resources. It behaves like a programmable stack of context managers and cleanup callbacks. Instead of nesting many with statements, you register each resource as it is acquired and let the stack release everything in reverse order, even when an exception interrupts the operation.
Why ExitStack exists
A normal with statement is ideal when the resources are known in advance. The problem changes when the number of files depends on user input, when a connection is optional, or when some cleanup logic is not implemented as a context manager. ExitStack handles all of these cases in a single block.
from contextlib import ExitStack
paths = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
files = [stack.enter_context(open(path, encoding="utf-8")) for path in paths]
texts = [file.read() for file in files]
Every file is closed automatically. If opening or reading one file fails, the files already acquired are still closed. Review the basics in using with in Python and Python exception handling.
How the exit stack works
Each registered resource contributes an exit action. When the block ends, actions run in LIFO order: the last one registered is the first one released. This matches the natural behavior of nested context managers.
The most common method is enter_context. It calls the context manager’s __enter__ method, returns its value, and stores its __exit__ method for later.
with ExitStack() as stack:
file = stack.enter_context(open("data.txt", encoding="utf-8"))
connection = stack.enter_context(create_connection())
Registering cleanup callbacks
Some resources do not implement the context manager protocol. The callback method registers a regular function plus its arguments. The function runs when the stack closes.
from pathlib import Path
from contextlib import ExitStack
folder = Path("temporary")
folder.mkdir(exist_ok=True)
with ExitStack() as stack:
stack.callback(folder.rmdir)
output = folder / "result.txt"
output.write_text("done", encoding="utf-8")
stack.callback(output.unlink)
The file deletion was registered last, so it runs first. The directory is removed only after it becomes empty. For file and path operations, see pathlib in Python.
Using push correctly
The push method registers an object’s __exit__ method, or any callable with a compatible signature. It does not call __enter__. This is useful when initialization already happened and you only need to transfer cleanup responsibility.
resource = Resource()
resource.start()
with ExitStack() as stack:
stack.push(resource)
resource.run()
Using enter_context in this situation could initialize the resource twice, so the distinction matters.
Optional resources
ExitStack makes conditional acquisition easy. A debug log, transaction, lock, or remote session can be added only when needed without duplicating the entire block.
with ExitStack() as stack:
if debug:
log = stack.enter_context(open("debug.log", "a", encoding="utf-8"))
log.write("started\n")
run_job()
You can also combine it with nullcontext when a uniform context-manager expression is convenient.
Dynamic file processing
A common use case is processing an arbitrary list of CSV files. ExitStack keeps all streams open for the required period and closes them reliably afterward.
import csv
from contextlib import ExitStack
def merge(paths):
with ExitStack() as stack:
files = [stack.enter_context(open(p, newline="", encoding="utf-8")) for p in paths]
readers = [csv.DictReader(f) for f in files]
return [row for reader in readers for row in reader]
Learn more in CSV files in Python.
Separating acquisition from commit
The pop_all method transfers all registered exit callbacks to a new stack without running them. This supports two-phase workflows: acquire and validate first, then decide whether cleanup should remain attached to the current scope.
stack = ExitStack()
try:
resources = [stack.enter_context(open(p, encoding="utf-8")) for p in paths]
validate(resources)
owner = stack.pop_all()
finally:
stack.close()
This pattern is useful for complex constructors, batch imports, and operations that must roll back completely after partial failure.
Exception behavior
Callbacks registered with callback do not receive exception details and cannot suppress an exception. Exit-style functions registered with push receive the exception type, value, and traceback and may suppress it by returning a true value.
Silent suppression should be used carefully because it can hide defects. Cleanup functions should be small, predictable, and preferably idempotent. If one cleanup action fails, later actions still need a chance to run, so avoid placing unrelated work inside a single callback.
ExitStack in tests
Tests often need a dynamic set of patches, temporary resources, or mocks. ExitStack keeps these objects in one visible scope and avoids deeply nested decorators.
from contextlib import ExitStack
from unittest.mock import patch
with ExitStack() as stack:
mock_a = stack.enter_context(patch("module.function_a"))
mock_b = stack.enter_context(patch("module.function_b"))
execute_flow()
For a broader introduction, see unit testing in Python.
AsyncExitStack
Asynchronous applications can use contextlib.AsyncExitStack. It provides enter_async_context, push_async_exit, and push_async_callback. Cleanup operations are awaited when the async with block finishes.
from contextlib import AsyncExitStack
async with AsyncExitStack() as stack:
session = await stack.enter_async_context(create_session())
channel = await stack.enter_async_context(open_channel())
This is valuable for network clients, asynchronous databases, queues, and services. See asyncio in Python.
Best practices
Use ExitStack when resource count is dynamic, resources are optional, or context managers and normal callbacks must be combined. For two or three fixed resources, a regular with statement is usually clearer.
Register cleanup immediately after acquiring each resource. Keep callbacks focused. Document any use of push because it skips __enter__. Do not treat ExitStack as a replacement for validation, logging, retries, or user-facing error handling. Its responsibility is lifecycle management.
Conclusion
contextlib.ExitStack turns resource cleanup into a flexible, explicit workflow. It preserves the safety of context managers while supporting runtime decisions, ordinary callbacks, optional resources, and staged ownership transfer. It is especially useful in automation scripts, file pipelines, tests, and services that coordinate several external resources.
Official references: Python ExitStack documentation and PEP 343.







