How do ExitStack.callback and push differ from enter_context on the same stack?
answer
- Three doors onto one list
- Which one calls __enter__?
- Which one sees the exception triple?
- Bind arguments, do not close over them
- Legacy close() needs the plain-function door
basics
~10 senter_context calls a manager's enter and registers its exit. push registers an exit-style callable without entering anything. callback registers a plain function plus arguments, called at unwind time and never shown exception details.
solid answer
~50 sAll three push one entry onto the same stack and unwind in the same reverse order; they differ in what they accept. `enter_context(cm)` is the full protocol: it invokes `type(cm).__enter__`, registers `__exit__`, and returns the entered value. `push(exit)` registers an object with `__exit__`, or a bare callable with that three-argument signature, **without** calling `__enter__` - use it when the resource is already open, or to attach an exit hook to a stack. `callback(fn, *args, **kwargs)` takes any plain callable and calls it with those bound arguments at unwind time; it receives no exception information and cannot suppress anything, which makes it the right tool for legacy objects that only expose `close()` or `disconnect()`. Bind the arguments through `callback` rather than capturing them in a lambda, or a loop variable's late binding will make every cleanup act on the last item.
code
python · 23 linesfrom contextlib import ExitStack
class Channel:
def __init__(self, name):
self.name = name
def disconnect(self):
print("disconnected", self.name)
def exit_hook(exc_type, exc, tb):
print("hook saw", exc_type)
return False
with ExitStack() as stack:
stack.push(exit_hook)
for name in ("smtp", "metrics"):
stack.callback(Channel(name).disconnect)
print("sending digest")
# sending digest
# disconnected metrics
# disconnected smtp
# hook saw Nonego deeper
Remember there are three ways in: enter_context for a context manager, callback for a plain function, push for an already-open resource or exit hook - and all three unwind together in reverse order.
Explain the differences precisely: which calls enter, which sees the exception triple, what each returns, and why arguments are bound through callback instead of captured in a lambda.
Show you use this to unify old and new resources in one teardown order, and that you know a callback cannot suppress, so error handling for legacy teardown has to live inside the callback itself.
Take a position on the house style: whether cleanup is expressed as context managers everywhere or adapted onto a stack, and how you stop long-lived stacks from silently accumulating callbacks in long-running services.
`contextlib.ExitStack` has one internal list of exit callbacks and three public ways to add to it. Choosing between them is a question of *what you are holding*, not of what you want cleanup to do. ## enter_context - the full protocol ```python handle = stack.enter_context(open(path, encoding="utf-8")) ``` `enter_context()` takes a context manager, calls its `__enter__`, registers its `__exit__` on the stack, and returns whatever `__enter__` returned. It is exactly `with cm as handle:` with the exit deferred to the stack. Use it whenever you have a real context manager that has **not** been entered yet. Because the manager's `__exit__` is registered, it receives full exception details at unwind time and can suppress the exception by returning a true value. ## push - register an exit without entering ```python stack.push(already_open_resource) # registers its __exit__ only stack.push(exit_hook) # a callable taking (exc_type, exc, tb) ``` `push()` **does not call `__enter__`.** It accepts either an object that has an `__exit__` - registering just that method - or a bare callable with the `__exit__` signature `(exc_type, exc_value, traceback)`. Two situations call for it. The first is a resource that is already open: something handed to you entered, whose teardown you now want the stack to own. The second is an exit hook that is not a resource at all - a function that wants to observe, or act on, whatever exception is in flight when the block ends. Because a pushed callable sees the exception triple and its return value is honoured, it is the only one of the three that can suppress. `push()` returns the object it was given unchanged, which is what allows it to be used as a decorator on an exit function. ## callback - a plain function with bound arguments ```python stack.callback(connection.disconnect) stack.callback(shutil.rmtree, workdir, ignore_errors=True) ``` `callback(fn, *args, **kwargs)` registers any callable, to be invoked with those arguments when the stack unwinds. It is the adapter for the enormous amount of real code that predates the context-manager protocol: objects with `close()`, `release()`, `disconnect()`, `unlink()`, or a module-level function that undoes a module-level change. The callback receives **no** exception information and its return value is ignored, so it cannot suppress an exception - it is pure teardown. Like `push()`, it returns the callable it was given. ## The late-binding trap Binding arguments through `callback()` is not a convenience, it is a correctness feature: ```python for channel in channels: stack.callback(lambda: channel.disconnect()) # WRONG stack.callback(channel.disconnect) # right ``` The lambda closes over the *variable* `channel`, not its value, so every registered cleanup disconnects whichever channel the loop finished on, and the rest are never closed. Passing the bound method - or `stack.callback(close_channel, channel)` - captures the value at registration time. This is the most common ExitStack bug in review. ## They interleave The three methods share one stack, so a manager entered with `enter_context()`, a legacy object registered with `callback()`, and an exit hook added with `push()` unwind in a single reverse-order sequence determined purely by registration order. That is what lets a stack manage a mixed set of old and new resources correctly, which a nested `with` statement cannot do without wrapper classes. ## Choosing, in one line each Not yet entered and has `__enter__`/`__exit__`: **`enter_context`**. Already entered, or you want a hook that sees the exception: **`push`**. Anything else that just needs a function called on the way out: **`callback`**. ## A lifetime caution Every registration keeps a strong reference to the callable and to the arguments bound with it, and holds them until the stack unwinds. On a short-lived stack that is invisible. On a stack that lives as long as the process and gains a callback per unit of work, it is unbounded memory growth in which no single object looks suspicious - the stack does. Scope the stack to the work, or call `close()` at the end of each cycle.
- Which of the three registration methods can suppress an exception, and why only that one?Only `push()` - and `enter_context()` indirectly, through the manager's own `__exit__`. Both register something that is called with `(exc_type, exc_value, traceback)` and whose truthy return value suppresses. A `callback()` function is called with only its bound arguments and its return value is discarded, so it can never swallow an exception.
- Why register connection.disconnect rather than a lambda that calls it?A lambda in a loop closes over the loop *variable*, so every cleanup ends up acting on the final iteration's object and the others are never released. Passing the bound method, or passing the function plus its arguments to `callback`, captures the value at registration time, which is what you meant.
saying these in an interview costs you the question
- Thinks push() calls __enter__ on what it is given
- Expects a callback() function to receive exception details
- Registers a lambda over a loop variable in a loop
- Believes only real context managers can go on a stack
- Says enter_context returns the manager rather than its entered value