Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

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.

Levels: Intermediate ~25 min read

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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)
Was this page helpful?

Mark this learning resource complete to track your learning progress.