Managing Resources with contextlib.ExitStack
Manage optional and dynamically selected synchronous or asynchronous resources with ExitStack and AsyncExitStack, cleanup callbacks, acquisition failures, and ownership transfer.
Intermediate
A normal
with statement is ideal when the resources are known in advance. Sometimes a program decides at runtime which files or other resources it needs.
contextlib.ExitStack lets the program register each context as it is acquired, then unwind all registered cleanup in reverse order—even when a later acquisition fails.
Why ExitStack helps with dynamic resources
Use
ExitStack when the number of context managers is variable or the resources are selected by a loop or condition. The stack itself is a context manager. Calling
enter_context(manager) enters that manager immediately and records its exit operation for the end of the stack. The value returned by the manager’s
__enter__() is returned to you.
Output
Ready to run.
The list comprehension can enter any number of files. If the list is empty, no files are opened and the stack still exits normally.
TemporaryDirectory is only used here to isolate the example’s files; this lesson focuses on the dynamic cleanup stack.
Register cleanup callbacks
Use
callback(function, *args, **kwargs) when cleanup is a function call rather than a context manager. The callback is registered immediately and called when the stack closes. Callbacks run in last-in, first-out order, matching nested context-manager cleanup. A callback registered with
callback() does not receive exception details and cannot suppress an exception.
Output
Ready to run.
Cleanup still runs when acquisition fails
Enter each resource through the same stack as soon as it is acquired. If opening a later resource raises an exception, the
with statement exits through the stack and closes resources that were already registered. This is safer than maintaining a separate list of open handles and trying to close only the ones that succeeded.
Output
Ready to run.
Combine callbacks and context managers
A single
ExitStack can own both context managers and ordinary cleanup callbacks. Every registration joins the same stack, so their reverse order makes dependencies explicit. Register a callback before entering a resource when that callback should run after the resource has exited; register it afterward when it must run first.
Output
Ready to run.
Transfer cleanup ownership with pop_all()
pop_all() transfers the registered callbacks to a new
ExitStack and returns that stack without executing cleanup. The original stack becomes empty. The code receiving the returned stack now owns the responsibility to close it, either with a with statement or by calling
close(). Do not discard the returned stack while expecting its callbacks to run.
Output
Ready to run.
Choose ExitStack when it improves clarity
For a fixed, small number of resources, ordinary nested or combined
with statements are usually easier to read. Prefer
ExitStack when the count is dynamic, resource acquisition is conditional, or setup has several independent cleanup steps. It is not a replacement for every
with statement and it does not rely on garbage collection to perform cleanup.
Output
Ready to run.
Use AsyncExitStack for mixed synchronous and asynchronous resources
The regular
ExitStack` enters synchronous context managers and registers their cleanup. When a runtime-selected group can contain both synchronous and asynchronous managers, use
AsyncExitStack` inside
async with`. Enter a synchronous manager with
stack.enter_context(manager)`, and enter an asynchronous manager with
await stack.enter_async_context(manager)`. Both join the same cleanup stack.
Output
Ready to run.
The output lists both values first, then shows that cleanup runs in reverse acquisition order.
AsyncExitStack` waits for asynchronous exit methods while also running ordinary synchronous context-manager exits. Its direct cleanup method is
aclose()`, not
close()`; using
async with` normally performs cleanup automatically.
Quick check
Which expression correctly enters an asynchronous resource and registers its asynchronous cleanup with AsyncExitStack?
import asyncio
from contextlib import AsyncExitStack
events = []
class SyncResource:
def __init__(self, name, events):
self.name = name
self.events = events
def __enter__(self):
self.events.append(f"enter {self.name}")
return self.name
def __exit__(self, exc_type, exc_value, traceback):
self.events.append(f"exit {self.name}")
return False
class AsyncResource:
def __init__(self, name, events):
self.name = name
self.events = events
async def __aenter__(self):
self.events.append(f"enter {self.name}")
return self.name
async def __aexit__(self, exc_type, exc_value, traceback):
self.events.append(f"exit {self.name}")
return False
async def main():
async with AsyncExitStack() as stack:
resource =
print(resource)
print(events)
asyncio.run(main())
Quick check
Which method enters a context manager now and registers its exit method?
from contextlib import ExitStack
events = []
class Resource:
def __init__(self, name):
self.name = name
def __enter__(self):
events.append(f"enter {self.name}")
return self.name
def __exit__(self, exc_type, exc_value, traceback):
events.append(f"exit {self.name}")
return False
with ExitStack() as stack:
events = []
stack.callback(events.append, "base")
result =
print(type(result).__name__)
if isinstance(result, ExitStack):
result.close()
print(events)
Quick check
Which method registers an ordinary cleanup function together with its arguments?
from contextlib import ExitStack, nullcontext
events = []
with ExitStack() as stack:
stack.callback(events.append, "base")
result =
print(type(result).__name__)
if isinstance(result, ExitStack):
result.close()
print(events)
Quick check
What should be used to enter a file through the stack in a loop?
import io
from contextlib import ExitStack
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as directory:
path = Path(directory) / "a.txt"
path.write_text("hello", encoding="utf-8")
with ExitStack() as stack:
def register_close(stack, path):
stack.callback(path.open(encoding="utf-8").close)
return None
file =
if file is None:
print("no file returned")
else:
print(type(file).__name__, file.read())
if file is not None:
print("closed when stack exited:", file.closed)
if not file.closed:
file.close()
Quick check
Which method transfers the registered cleanup operations to a new stack without running them?
from contextlib import ExitStack, nullcontext
events = []
with ExitStack() as stack:
stack.callback(events.append, "cleanup")
selected =
print(type(selected).__name__)
print("after original stack:", events)
if isinstance(selected, ExitStack):
selected.close()
print("after transferred stack:", events)
Quick check
Which statement about callback cleanup order is correct?
from contextlib import ExitStack
events = []
with ExitStack() as stack:
stack.callback(events.append, "first")
stack.callback(events.append, "last")
selected =
print(events)
print("selected:", selected)
print("selected callback ran first:", events[0] == selected)
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.