Batu Lab NotesPractical developer guides

Transfer cleanup ownership with ExitStack.pop_all

By Batu ยท English technical notes

ExitStack.pop_all() transfers registered cleanup callbacks to a new stack. Therefore, leaving the original acquisition scope does not release the transferred resources; cleanup happens only when the returned stack is closed or used as a context manager.

The fixture registers two callbacks on original: one appends "first" and the other appends "second". It then calls pop_all() while still inside the original with block. The event list is empty both inside and after that block, proving that the original stack no longer owns callbacks to run. Closing transferred finally executes them in reverse registration order, producing second|first.

This pattern is useful after an acquisition-and-validation phase succeeds but another component must control the later lifetime. Keep a reference to the returned stack or to its close method; otherwise no cleanup is scheduled automatically merely because the new stack becomes unreachable. Conversely, do not close both stacks expecting duplicate cleanup: ownership moved, so the original is intentionally empty.

ExitStack and pop_all are available from Python 3.3. The standard documentation describes pop_all for all-or-nothing acquisition and states that an ExitStack invokes callbacks in LIFO order. This example only demonstrates callback ownership and ordering; a real transfer also needs a clear policy for who closes the returned stack and when.

See the Python ExitStack.pop_all documentation.

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

from contextlib import ExitStack


events = []
with ExitStack() as original:
    original.callback(events.append, "first")
    original.callback(events.append, "second")
    transferred = original.pop_all()
    assert events == []
    print("inside_original=empty")

assert events == []
transferred.close()
assert events == ["second", "first"]
print(f"after_transferred_close={'|'.join(events)}")
inside_original=empty
after_transferred_close=second|first