Python Context Manager Interview Questions: Cleanup, Exceptions, and ExitStack
Quick Overview
Practice Python context manager interviews with exception traces, partial acquisition, ExitStack cleanup, ownership transfer, and async resource scenarios. Cleanup boundaries protect only resources whose ownership has actually been registered.
Python context manager interview questions are about ownership: who acquires a resource, who releases it, and what happens if work fails between those two events. The with statement makes that lifecycle explicit, but it does not automatically fix a resource leak inside a failing __enter__ method or an exception swallowed by a careless __exit__.
This guide builds from the protocol to an original multi-file import exercise. The examples use ordinary Python 3 context-manager behavior; async cleanup and exception groups have additional version-sensitive details. Official Python documentation defines the protocol, while the scenarios here are practice designs rather than reported employer questions.
PracHub's question comparing generators, decorators, and context managers is a useful bridge from syntax to explanation. Use this article to go deeper on partial acquisition, exception propagation, and ExitStack.

What does the with statement actually guarantee?
A context manager supplies __enter__ and __exit__. After successful entry, Python arranges to call the exit method when the suite finishes, including ordinary exception and early-return paths. The value returned by __enter__ is what the as target receives; it need not be the manager itself.
On normal completion, __exit__ receives three None values. When the body raises, it receives the exception type, exception instance, and traceback. Returning a truthy value suppresses that exception. Returning false or None lets it propagate, unless the exit method raises a different exception.
Do not summarize this as "with catches errors." It establishes a protocol around a block. Whether an error is handled, logged, transformed, or allowed through depends on the manager's implementation.
It also does not promise cleanup after process termination by SIGKILL, a machine crash, or forced interpreter exit that skips normal unwinding. Persistent resources need recovery policies as well as in-process cleanup. A lock file left by a crashed process and an open Python file object are different problems.
Can you trace entry, body, and exit in order?
Use a tiny manager that makes the lifecycle visible:
class Trace:
def __init__(self, name):
self.name = name
def __enter__(self):
print("enter", self.name)
return self.name
def __exit__(self, exc_type, exc, tb):
print("exit", self.name, exc_type is not None)
return False
with Trace("A") as a, Trace("B") as b:
print("body", a, b)
The output order is enter A, enter B, body A B, exit B False, exit A False. Cleanup unwinds in reverse acquisition order. That order is useful when B depends on A remaining available during B's cleanup.
Now raise ValueError in the body. Both exit methods observe an active exception and the error propagates afterward because neither suppresses it. Change B to return true and A receives a normal-completion exception state instead. The outer manager sees the result of the inner manager's handling, not a permanently frozen copy of the original error.
This trace is a better interview answer than memorizing method signatures. It shows you understand the sequence and how nested cleanup affects exception flow.
What happens if enter fails halfway through?
If a manager's __enter__ raises, that manager's __exit__ is not automatically called. Entry never completed. Resources acquired earlier inside that failing method must therefore be protected by its own cleanup logic.
Suppose __enter__ opens a file and then acquires a network connection. If connection acquisition fails, the already-open file can leak unless entry unwinds it. Moving both steps into __enter__ did not make them transactional.
Contrast that with with A(), B():. If A entered successfully and B's entry fails, A's exit still runs. The two cases differ because A had already established its cleanup boundary, whereas B had not.
| Failure point | Cleanup registered? | Expected responsibility |
|---|---|---|
| Before acquisition | No | Report acquisition failure |
| Inside failing enter | Not by with for that manager | Enter releases partial resources |
| Later manager fails | Yes for earlier managers | Earlier managers unwind |
| Body raises after entry | Yes | Exit methods run in reverse order |
When reviewing code, mark the exact line where ownership transfers. A resource created one line before a guard is a common failure window. The goal is to make that window small and deliberate, not to assume exceptions only happen in the main body.

When is contextlib.contextmanager clearer than a class?
Use @contextmanager when acquisition, one yield, and cleanup form a simple readable lifecycle. A class is often clearer when the manager has substantial state, several methods, or a reusable object contract. Neither is inherently more correct.
from contextlib import contextmanager
@contextmanager
def traced_resource(acquire, release):
resource = acquire()
try:
yield resource
finally:
release(resource)
The generator must yield exactly once. Code before yield performs entry; code after yield participates in exit. An exception from the body is raised back at the yield point. If you catch it merely to log it, re-raise it unless suppression is the intended contract.
The finally starts after acquisition succeeds in this example. If acquire() itself partially allocates resources and then raises, its own implementation still needs to clean up. A wrapper cannot release a resource it never received or learned about.
Avoid writing a context manager that catches every exception and prints "done." That can make a failed import appear successful. Cleanup and success are different outcomes: a file can close correctly while the operation using it fails.
How does ExitStack manage a variable number of open files?
ExitStack lets you register cleanup incrementally when the number of resources is determined at runtime. Its callbacks unwind in last-in, first-out order. enter_context enters a manager and records its exit only after successful entry.
Here is an original exercise: open every requested UTF-8 file, count all lines, and close everything if any open or read fails.
from contextlib import ExitStack
def count_lines(paths):
with ExitStack() as stack:
files = [stack.enter_context(open(path, encoding="utf-8"))
for path in paths]
return sum(1 for file in files for _ in file)
If the third file fails to open, the first two files are closed. If decoding fails during iteration, all successfully entered files still close. The exception is not converted into a partial count.
This design deliberately opens all files at once, which may exceed descriptor limits for a large list. If the contract does not require simultaneous access, process one file at a time instead. ExitStack is a lifecycle tool, not permission to ignore resource capacity.
An interviewer may ask for all-or-nothing validation before processing. Opening all files proves only that opening succeeded at that time; it does not guarantee future reads will succeed or that files will remain unchanged. State the exact guarantee your design provides.
What is the difference between callback, push, and enter_context?
Use enter_context for an ordinary context manager that you want the stack to enter. Use callback for a cleanup function and arguments, such as releasing a resource that lacks the protocol. Use push when registering an exit-style callable or a manager's exit without having the stack perform entry.
The distinction matters for exception handling. A plain callback registered with callback is not passed the active exception details and cannot suppress it by returning true. An exit-style function registered with push participates in the exit protocol.
Do not treat these methods as interchangeable aliases. If you register a close function as an exit method, its signature may be wrong. If you enter a manager manually and also call enter_context, you may acquire it twice.
pop_all() transfers the stack of callbacks to another ExitStack without executing them. That enables an ownership handoff, but the new owner must eventually close the returned stack. Dropping the reference is not a reliable cleanup strategy; ExitStack does not promise to invoke callbacks just because it is garbage-collected.

How should I design an all-or-nothing importer?
Extend the file exercise: read several inputs, validate records, and write one combined output. On any input failure, the old published output must remain usable. Closing file handles is necessary but insufficient because it does not undo bytes already written to the final destination.
Separate resource cleanup from business commit. Use managed input handles and a temporary output owned by the operation. Validate the complete output before an appropriate final replacement step. Specify same-filesystem requirements and durability needs rather than casually equating rename with a complete transaction.
Your lifecycle now has two outcomes: failure removes temporary work, success transfers the completed artifact into its public location. An ExitStack can register cleanup and support a deliberate ownership transfer, but the surrounding storage semantics still determine atomicity and crash recovery.
Test four points: second input open fails, decoding fails halfway through, output writing fails, and final replacement fails. For each, name which resources are closed and which public artifact remains. This exercise exposes whether you understand the difference between "no leaked handles" and "no partial business result."
Should exit suppress exceptions or return false?
Return false by default unless the manager's purpose includes handling a specific, expected exception. Broad suppression makes callers unable to distinguish success from failure. A manager used only for cleanup generally should not redefine the operation's result.
For example, suppressing a missing optional cache file may fit the contract. Suppressing permission errors, malformed configuration, and connection failures together usually destroys useful information. Name the exact exception class and the fallback behavior.
If cleanup raises while another exception is active, the cleanup failure can become the propagated exception, with the original available through exception chaining. Logging and error reporting should preserve the relationship. Do not promise that the original error always remains the top-level exception.
A good review question is: "What will the caller observe if both the body and cleanup fail?" Answer it with a small trace. This is especially valuable for transaction-like managers where rollback itself can fail and operational diagnosis depends on both facts.
What changes with async context managers?
Async managers use __aenter__ and __aexit__, awaited by async with. They are appropriate when acquisition or release needs asynchronous work, such as opening an async connection or closing an async generator. A synchronous context manager does not become async-safe merely because it appears inside an async function.
Use AsyncExitStack when dynamically composing asynchronous and synchronous cleanup. Keep cancellation in the contract. Cleanup code that awaits can itself encounter cancellation or another failure; a robust design needs a deliberate policy rather than an absolute claim that cleanup can never be interrupted.
Also distinguish closing a connection from committing a transaction. A library may provide different managers for those responsibilities. Read the specific driver's contract before saying that every successful with connection commits or that every exit closes the connection.
For an interview, explain the lifecycle first and then the await points. You do not need to invent a large asynchronous framework to show that you understand ownership and exception propagation.
How can I practice cleanup reasoning effectively?
Write a fake resource that records acquisition and release into a list. Make each acquisition and each use step optionally raise. Assert the exact event order and final exception rather than merely checking that the function returned.
Your essential tests are normal completion, body error, early return, failure during the second acquisition, explicit suppression, and cleanup failure. For a transferred ExitStack, test that the original stack no longer closes the resources and that the new owner does.
Then explain the importer without code: which object owns each handle, when ownership changes, what is committed, and what survives a crash. Use the PracHub comparison question to rehearse that explanation alongside generators and decorators. You are ready when every exit path has a named owner and an observable result.
Sources and Further Reading
Checked September 23, 2026 against the current Python 3 documentation. Library-specific transaction behavior is intentionally not generalized.
Comments (0)