skip to content

Does a with statement catch exceptions raised in its body, or only run cleanup?

level: juniorimportance: must knowfreq 55%

answer

  1. Two separate promises, not one
  2. Teardown is guaranteed; handling is not
  3. The manager decides, not the statement
  4. Look at what __exit__ returns
  5. Falling off the end returns None

basics

~20 s

Only cleanup. The with statement calls the manager's exit on the way out, so the file gets closed or the lock released, but the exception keeps travelling up unless exit deliberately returns a truthy value.

solid answer

~40 s

A `with` statement guarantees that the manager's `__exit__` runs, not that the error disappears. When the body raises, Python calls `__exit__(exc_type, exc_value, traceback)` with the live exception, and then looks at what `__exit__` returned: a truthy value means "I handled it, stop propagating"; `None` or any falsy value means "keep unwinding". Managers written for cleanup - the object returned by `open()`, `threading.Lock`, `tempfile.TemporaryDirectory` - return `None`, so the exception reaches your caller exactly as if there had been no `with` at all. Seeing the exception is not the same as catching it: `__exit__` is handed the three arguments so it can *react* to failure, for example rolling back instead of committing. If you actually want to handle the error, wrap the `with` in `try`/`except`, or put the `try`/`except` inside the body.

code

python · 13 lines
python
class Noisy:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        print("cleanup ran")


try:
    with Noisy():
        raise ValueError("boom")
except ValueError as err:
    print("still propagated:", err)

go deeper

for a junior

Be ready to state plainly that with guarantees the file is closed or the lock released, and that the exception still reaches your caller. Knowing the difference between cleanup and handling is the whole question.

for a middle

Explain the mechanics: __exit__ is called with the exception triple, and its return value decides propagation, with None meaning keep unwinding. Name a standard manager that returns None and say why that is the right default.

for a senior

Show the judgement of placement - try/except outside the with versus inside it - and explain what conditional teardown (commit versus rollback) buys you without changing what the caller sees.

for a principal

Own the convention across a codebase: managers written by your team should react to failure and propagate it, and any manager that suppresses needs a documented, narrow reason. Errors that vanish inside shared utilities are the expensive kind.

A lot of newcomers read `with` as a safer `try`/`except`. It is not. `with` is a **cleanup** construct: it promises that a paired piece of teardown code runs whether the body finishes normally, returns, breaks, or raises. Whether the *error* survives is a separate decision, and it belongs to the context manager, not to the `with` statement. ## What actually happens on the failure path A context manager is any object with `__enter__` and `__exit__`. When the body raises, the interpreter does roughly this: 1. It catches the exception internally, so it can run teardown. 2. It calls `__exit__(exc_type, exc_value, traceback)` - the class of the exception, the instance, and its traceback object. 3. It inspects the **return value** of that call. 4. If the return value is truthy, the exception is discarded and execution continues on the line after the `with` block. If it is falsy - which includes the `None` you get from a function with no `return` - the exception is re-raised and keeps propagating. So the only lever that suppresses anything is that return value. Everything else about `__exit__` is observation. ## Why the default is "propagate" A Python function that falls off the end returns `None`, and `None` is falsy. That default is deliberate: a manager author has to opt *in* to swallowing errors. Nearly every manager in the standard library declines. The file object from `open()` closes the file and returns `None`. `threading.Lock` releases and returns `None`. A database transaction manager rolls back and returns `None`. Each of them reacts to the failure and then lets it continue to whoever is in a position to decide what the failure means. ## Seeing versus catching The three arguments exist so cleanup can be *conditional*. A transaction manager commits when `exc_type is None` and rolls back otherwise; a timing manager can log "failed after 4.2s" instead of "completed in 4.2s". None of that changes whether the caller sees the error. ```python class Transaction: def __enter__(self): return self def __exit__(self, exc_type, exc, tb): if exc_type is None: self.commit() else: self.rollback() # no return -> None -> the exception keeps propagating ``` That manager reacts to every failure and hides none of them, which is what you almost always want. ## The consequences people trip over Because the exception continues, the statements *after* the `with` block do not run either - the frame unwinds normally. A junior who assumes `with open(path) as f:` will "handle" a missing file is writing code that dies at the `open()` call, before the body even starts. And a manager that closes a resource does not make the operation succeed; it only makes the resource release deterministic. ## How to actually handle the error Two placements, and they mean different things: ```python # Handle it outside: cleanup runs first, then you decide. try: with open("cities.csv") as f: rows = parse(f) except (OSError, ValueError) as err: logging.warning("batch input unusable: %s", err) # Handle it inside: the manager stays open for the rest of the body. with open("cities.csv") as f: try: rows = parse(f) except ValueError: rows = [] ``` The outside form abandons the resource as part of failing. The inside form keeps working with it. Neither one requires a custom `__exit__`. ## The one exception to the rule You *can* write a manager that suppresses - that is exactly what a truthy `__exit__` return does, and the standard library ships one whose whole job is suppression. But that is a deliberate, narrow choice, and a manager whose primary purpose is cleanup should not do it. If you find yourself unable to explain which exception types your manager swallows and why, it should be returning `None`. ## Interview framing The crisp answer is one sentence with a caveat: "`with` guarantees cleanup, not handling - the exception propagates unless the manager's `__exit__` returns a truthy value, and standard managers return `None`." That sentence separates the three ideas an interviewer is probing: that teardown is guaranteed, that `__exit__` sees the exception, and that suppression is opt-in.

  • If the exception propagates anyway, why does __exit__ receive exc_type, exc_value and traceback at all?
    So cleanup can be conditional. A transaction manager commits when `exc_type is None` and rolls back otherwise; a timing or metrics manager records a failure rather than a success. The arguments let the manager *react* to the error without deciding the caller's fate - it can inspect all three and still return `None` so the exception continues.
  • Does __exit__ still run if the body executes a return or break instead of raising?
    Yes. `__exit__` runs on every exit path from the block - normal fall-through, `return`, `break`, `continue`, and exceptions. On the non-exception paths all three arguments are `None`, and the return value is ignored, because there is nothing to suppress. That guarantee is the whole point of using `with` instead of manual cleanup.
  • Where should the try/except go if you want the resource still usable while handling the error?
    Inside the `with` body. Wrapping the `with` statement means the manager tears down first and you handle the error with the resource already gone; putting the `try`/`except` inside the body keeps the file, lock or connection open so you can log, retry or write a fallback record before the block ends.

A with block is like a valet who always parks the car and always brings it back - even when you crashed it. Bringing the car back is not the same as fixing the dent, and the dent still shows up in your driveway.

saying these in an interview costs you the question

  • Claims with is a shorthand for try/except
  • Says the with statement swallows errors from its body
  • Thinks code after the with block still runs after an exception
  • Believes cleanup is skipped when the body raises
  • Confuses __exit__ receiving the exception with handling it

context