If a context manager's __enter__ raises, is its __exit__ still called?
answer
- Cleanup is armed, not assumed
- The promise is per-manager
- Acquisition must unwind itself
- Already-entered managers still exit, in reverse
- Nothing runs for a manager that never entered
basics
~20 sNo. The with statement only commits to calling exit once enter has returned successfully, so a manager that fails partway through acquisition must clean up after itself inside enter or leak whatever it already took.
solid answer
~40 s`__exit__` is registered only after `__enter__` returns. If `__enter__` raises, the exception propagates straight out of the `with` statement, the body never runs, and that manager's `__exit__` is never called - so anything `__enter__` had already acquired before failing is leaked. The fix belongs inside `__enter__`: wrap the multi-step acquisition in `try`/`except`, release what you took, and re-raise. Ordering across several managers follows the same rule and is the part people get wrong: in `with A() as a, B() as b:`, if `B.__enter__` raises then `A` was already entered, so `A.__exit__` *is* called with the exception triple, and managers unwind in reverse order of entry. So the guarantee is per-manager: every manager that successfully entered gets its `__exit__`, and no manager that failed to enter does.
code
python · 21 linesclass Tracked:
def __init__(self, name, fail=False):
self.name = name
self.fail = fail
def __enter__(self):
print("enter", self.name)
if self.fail:
raise RuntimeError(self.name + " failed to acquire")
return self
def __exit__(self, exc_type, exc, tb):
print("exit", self.name)
return False
try:
with Tracked("A") as a, Tracked("B", fail=True) as b:
print("body runs")
except RuntimeError as err:
print("raised:", err)go deeper
Remember the one-line rule: no successful __enter__, no __exit__. If setup fails, the block never runs and the manager gets no cleanup call at all.
Explain why - cleanup is armed only after __enter__ returns - and show the exception-safe __enter__ that releases what it already acquired and re-raises. Be able to state the ordering when one of several managers fails to enter.
Connect it to production symptoms: a manager with multi-step acquisition leaks connections or file handles on every retry, and the traceback points at the failing step rather than the leak. Review __enter__ implementations for that pattern.
Set the expectation that resource acquisition in shared managers is all-or-nothing, and prefer composing single-resource managers over one manager that acquires several things, so the language's own ordering guarantees do the unwinding instead of hand-written cleanup.
The `with` statement's promise is narrower than it first appears. It is not "this manager will always be cleaned up"; it is "**a manager that successfully entered will always be exited**". The gap between those two sentences is where resource leaks live. ## The mechanic Executing a `with` statement does three things in order: evaluate the manager expression, look up `__enter__` and `__exit__` on its type, and call `__enter__`. Only after `__enter__` returns does the interpreter arm the cleanup that will call `__exit__`. So if `__enter__` raises: - the exception propagates out of the `with` statement itself, - the body never executes, - the `as` target is never bound, - and `__exit__` is never called for that manager. There is no partial-cleanup magic. The interpreter has no idea what `__enter__` acquired before it failed. ## Why that leaks, and whose job it is A realistic manager acquires more than one thing: ```python class GeocodeBatch: def __enter__(self): self.conn = connect(self.dsn) # step 1 succeeds self.output = open(self.path, "w") # step 2 raises OSError return self ``` If step 2 raises, the connection from step 1 is now unreferenced and unclosed, and `__exit__` will never run to close it. Over a six-hour nightly run that retries the batch every few minutes, that is a steady drip of connections until the pool is exhausted - and the traceback points at the file open, not at the leak. Ownership is unambiguous: **`__enter__` must be exception-safe on its own.** Acquisition is either all-or-nothing, or it cleans up what it took: ```python def __enter__(self): self.conn = connect(self.dsn) try: self.output = open(self.path, "w") except BaseException: self.conn.close() raise return self ``` Catch `BaseException` rather than `Exception` here, because a `KeyboardInterrupt` arriving mid-acquisition leaks exactly the same way. Re-raise bare so the original exception and traceback are preserved. ## Ordering with several managers The per-manager rule answers the ordering question directly. `with A() as a, B() as b:` is equivalent to nesting: `A` is entered, then `B`. If `B.__enter__` raises: 1. `A.__exit__(exc_type, exc, tb)` is called, with `B`'s exception, because `A` did enter successfully. 2. `B.__exit__` is not called, because `B` never entered. 3. Whatever `A.__exit__` returns decides whether `B`'s acquisition error propagates - a truthy return from `A.__exit__` would swallow it, which is one more reason unconditional suppression is dangerous. Managers always unwind in reverse order of entry, so cleanup mirrors acquisition. The same holds for `try`/`finally` written by hand, and for any dynamic-cleanup helper: each resource is only registered for release once its acquisition succeeded. ## The mirror case: failing before `__enter__` There is a second gap that is easy to miss. If the expression that *constructs* the manager raises - `open("/missing/path")` inside `with open("/missing/path") as f:` - there is no manager object at all, so neither `__enter__` nor `__exit__` runs. This is why `with open(...)` does not protect you from a missing file: that error happens before the statement has anything to manage. ## How to answer Lead with the rule and the reason: `__exit__` is armed only after `__enter__` returns, so a manager that fails mid-acquisition must unwind itself. Then volunteer the multi-manager ordering, because that is what distinguishes someone who has memorised the sentence from someone who has reasoned about it: already-entered managers *do* get their `__exit__`, in reverse order, and they receive the exception that came out of the failing `__enter__`. ## Spotting it in review Three cheap checks catch nearly every instance. First, does `__enter__` contain more than one acquiring call - a connect, an `open`, an `acquire`, a `mkdtemp`? If so it needs its own guard. Second, does the class store acquired handles on `self` before it can fail again, with `__exit__` as the only place they are released? That is the leak signature. Third, is the manager acquiring several unrelated resources at once? Composing one manager per resource, entered in sequence, hands the ordering problem back to the language: each nested manager that entered gets its `__exit__`, in reverse order, with no hand-written unwinding to get wrong. The version that leaks is almost always the version somebody wrote to save a level of nesting.
- How should __enter__ be written when acquisition takes several steps that can each fail?Guard the later steps and release the earlier ones: acquire step one, then `try` the rest with `except BaseException:` that closes what you already hold and re-raises bare. Catching `BaseException` matters because a `KeyboardInterrupt` mid-acquisition leaks identically, and a bare `raise` preserves the original exception and its traceback.
- In `with A() as a, B() as b:`, B.__enter__ raises. Which __exit__ methods run, and with what arguments?Only `A.__exit__`, and it receives the exception triple from `B.__enter__`'s failure - the type, the instance and its traceback. `B.__exit__` never runs because `B` never entered. Managers unwind in reverse order of entry, so cleanup mirrors acquisition, and a truthy return from `A.__exit__` would suppress `B`'s acquisition error.
- Does `with open(path) as f:` protect against the file not existing?No. The `open(path)` call is evaluated before the `with` statement has a manager at all, so a missing file raises `FileNotFoundError` there - no `__enter__`, no `__exit__`, no body. The `with` statement only governs what happens once a manager object exists and has entered successfully; failures during construction are outside its scope.
saying these in an interview costs you the question
- Says __exit__ always runs once the with statement starts
- Thinks the interpreter cleans up whatever __enter__ acquired
- Believes no manager is exited when one __enter__ fails
- Expects the as target to be bound after a failed __enter__
- Cannot say which order multiple managers unwind in