ExitStack records cleanup actions and runs them in reverse registration order when its context exits.
Python ExitStack: unwind partially acquired resources
Operation contract
The import acquires two owned in-memory streams before a third acquisition fails. Previously acquired streams close through the stack even though the main import never starts. Register cleanup immediately after each successful acquisition. A separate close callback records order so the failure path can be inspected without using a real service.
Failure and ownership boundary
ExitStack.callback does not suppress an exception. Context managers entered through enter_context may have different exit behavior. A callback that raises can change the visible failure, so cleanup itself needs a failure policy. The stack does not close resources merely because it is garbage collected. Python context managers: clean up on success and failure and Python exceptions: translate an input error without hiding its cause supply the underlying rules.
Working program
from contextlib import ExitStack
from io import StringIO
closed = []
def release(stream, label):
stream.close()
closed.append(label)
streams = []
try:
with ExitStack() as stack:
for label in ("receipts", "prices"):
stream = StringIO(label)
streams.append(stream)
stack.callback(release, stream, label)
raise OSError("third input unavailable")
except OSError:
print("acquisition failed")
print(closed)
print(all(stream.closed for stream in streams))Output
acquisition failed
['prices', 'receipts']
TrueCosts and limits
Registering k callbacks retains O(k) cleanup state. Unwinding takes the sum of their costs. Resource acquisition and close latency are external to the stack bookkeeping and may block.
Common Mistakes
- Register cleanup before attempting the next acquisition.
- Do not depend on stack garbage collection to release resources.
Connected lessons
Python context managers: clean up on success and failure, Python pathlib files: specify encoding and close the resource owner, Python exceptions: translate an input error without hiding its cause.
Follow the ownership and update boundary
Python ExitStack pop_all: transfer cleanup without discarding it.
