Batu Lab NotesPractical developer guides

Use ExitStack to unwind partially acquired resources

By Batu · English technical notes

Also published in our primary archive.

Use ExitStack when resources are acquired dynamically: if a later __enter__ fails, contexts already entered are unwound in reverse order. This lets a test reproduce partial acquisition without files, sockets, or external services.

The synthetic Resource context manager appends lifecycle strings to a list. one and two enter successfully. three records its attempted entry and raises RuntimeError before it has supplied a resource to the stack. The surrounding ExitStack then exits the contexts that were successfully registered: first two, then one. The caught error is also asserted, so the test does not mistake normal completion for a cleanup path.

The exact event list is the important observation. It shows that the failing resource has no matching exit because its __enter__ did not finish, while each previously acquired resource receives exactly one exit in LIFO order. It does not prove that a particular external resource is safe to close twice or that its own __exit__ implementation is correct; those are contracts of the real resource.

ExitStack was added in Python 3.3. Its callbacks run in reverse registration order on close() or context exit, and enter_context registers a context manager’s exit only after entering it. This makes it suited to variable-size acquisition flows.

See the Python ExitStack documentation.

AI-assistance disclosure: AI helped draft this synthetic example and explanation.

from contextlib import ExitStack


class Resource:
    def __init__(self, name, events, fails=False):
        self.name = name
        self.events = events
        self.fails = fails

    def __enter__(self):
        self.events.append(f"enter:{self.name}")
        if self.fails:
            raise RuntimeError(self.name)
        return self

    def __exit__(self, exc_type, exc, traceback):
        self.events.append(f"exit:{self.name}")
        return False


events = []
try:
    with ExitStack() as stack:
        stack.enter_context(Resource("one", events))
        stack.enter_context(Resource("two", events))
        stack.enter_context(Resource("three", events, fails=True))
except RuntimeError as error:
    failure = str(error)

assert failure == "three"
assert events == [
    "enter:one", "enter:two", "enter:three", "exit:two", "exit:one"
]
print(f"failure={failure} events={'|'.join(events)}")
failure=three events=enter:one|enter:two|enter:three|exit:two|exit:one