skip to content

questions

3

In Python, what does a `return` inside a `finally` block do to the value the `try` block was returning?

level: juniorimportance: must knowfreq 52%

answer

  1. Two exits competing for one function
  2. finally runs with something already pending
  3. The pending outcome gets thrown away
  4. Exceptions vanish too, not just values
  5. Python 3.14 warns at compile time

basics

~20 s

A return inside finally overrides everything already in flight: the try block's return value, a pending break or continue, and even an exception on its way out. The function returns the finally value, and the exception disappears silently.

solid answer

~40 s

`finally` runs on every exit path, and the interpreter holds the reason it is leaving — a return value, a loop jump, or a propagating exception — while the block executes, resuming it only if the block ends normally. A `return` in `finally` ends the function then and there, so the pending outcome is discarded: `try: return "try" / finally: return "finally"` yields `"finally"`, and a `try` block that raised returns the `finally` value with no traceback at all. Note the narrower case: rebinding the local after `return total` changes nothing, because the value was already evaluated. Python 3.14 added a compile-time `SyntaxWarning` for this under PEP 765; behaviour is unchanged. Keep result decisions in `try`/`except` and leave `finally` for cleanup.

code

python · 8 lines
python
def pick():
    try:
        return "try"
    finally:
        return "finally"


print(pick())  # finally

go deeper

for a junior

Recall the headline rule: a return inside finally replaces the try block's return value and also throws away any exception that was propagating. Be ready to trace a four-line function out loud and say what it returns.

for a middle

Explain the mechanism, not just the outcome: finally runs with the exit reason held pending, and a return there replaces it. Distinguish rebinding a local (no effect) from executing a second return (total override).

for a senior

Show you would catch this in review and in CI. Name Python 3.14's PEP 765 SyntaxWarning, promote it to an error in the pipeline, and explain the production risk of silently swallowing exceptions, including KeyboardInterrupt and SystemExit.

for a principal

Own the policy question: whether a compile-time warning like this should be an error across every service, what a codebase-wide sweep costs, and how you weigh a future release turning it into a hard SyntaxError against churn today.

`finally` exists to guarantee that a block of cleanup runs no matter how the `try` block is left: normally, via `return`, via `break` or `continue`, or with an exception on its way out. To make that guarantee, the interpreter records *why* control is leaving — the pending outcome — runs the `finally` body, and then resumes that outcome. A `return` inside the `finally` body destroys the record. Control leaves the function immediately with the new value, and whatever was pending is discarded without a trace. ### The pending outcome is a single slot It helps to think of exactly one "reason for leaving" being carried at a time, and `finally` being allowed to overwrite it. * If the `try` block was returning `x`, that pending return is replaced. * If a `break` or `continue` was in flight, the loop jump is cancelled and the function returns instead. * If an exception was propagating, it is **swallowed**: no traceback, no logging, no `__context__` chain, nothing. The caller sees an ordinary successful return. That last case is what makes this a real production defect rather than a puzzle. A validation error, a timeout, even `KeyboardInterrupt` or `SystemExit` — all of them are dropped identically, because `finally` runs for every exit path and a `return` there ends the function unconditionally. ```python def swallow(): try: raise ValueError("gone") finally: return "clean" swallow() # 'clean' -- the ValueError never reaches the caller ``` ### The returned value is computed *before* finally runs A closely related question is what happens if the `finally` block modifies the local the `try` block returned. The answer is: nothing. `return total` first *evaluates* `total` and hands that object to the return machinery; only then does `finally` run. Rebinding the name afterwards changes the name, not the value already in flight: ```python def pick(): total = 1 try: return total finally: total = 99 pick() # 1 ``` The distinction is between *rebinding a name* (no effect on the pending return) and *executing a new `return` statement* (total override). If the returned object is mutable and `finally` mutates it in place, the caller does see the mutation — the same object is returned, in its new state. That is a mutation question, not a control-flow one. ### What Python 3.14 changed The semantics above are old and unchanged. What changed is the diagnostics. Python 3.14 implements PEP 765, which makes the compiler emit a `SyntaxWarning` whenever `return`, `break` or `continue` would transfer control *out of* a `finally` block: ``` app.py:5: SyntaxWarning: 'return' in a 'finally' block ``` Three things are worth knowing about it. First, it is a **compile-time** warning, produced when the module is compiled, not when the function runs — so it fires even for code paths never taken, and it is cached away with the `.pyc` on subsequent runs unless the source changes. Second, it is only a warning: the code still compiles and still behaves exactly as before, so 3.14 breaks nothing. Third, it can be promoted to a hard failure, which is the right move in CI: `python -W error::SyntaxWarning` or `PYTHONWARNINGS=error::SyntaxWarning` turns it into a `SyntaxError` at compile time. PEP 765 explicitly leaves the door open to making this an error in a future release, so treating it as one now costs nothing. On 3.13 and earlier the same code compiles silently; static analysers have flagged the pattern for years, which is why teams with a strict linter never hit it. The warning is scoped precisely: it fires only for statements that *leave* the `finally` block. A `return` inside a nested function defined in the `finally` body, or a `break` belonging to a loop written entirely inside the `finally` body, is not flagged, because neither one exits the block. ### How to write it instead `finally` should be for cleanup with no opinion about the outcome. Any statement that decides what the function returns belongs in `try` or in an `except` handler, where it is visible and where an exception you did not expect still escapes: ```python def pick(): try: return compute() except ValueError: return fallback() # deliberate, and typed finally: release_resources() # cleanup only, no return ``` If you genuinely want "return a default no matter what happened", say so with `except Exception:` and a `return` in the handler. That is the same outcome written honestly: the reader can see which exceptions are being absorbed, `BaseException` subclasses such as `KeyboardInterrupt` still propagate, and no future maintainer has to reason about a hidden control-flow override buried in a cleanup block.

  • If the `try` block runs `return total` and the `finally` block then rebinds `total`, what is returned?
    The original value. `return total` evaluates the expression and hands that object to the return machinery before `finally` runs, so rebinding the name afterwards has no effect. Only executing a new `return` statement inside `finally` overrides it. If the returned object is mutable and `finally` mutates it in place, the caller does see that mutation — but that is the same object being returned, not a changed return value.
  • Does a `return` in `finally` also swallow a `KeyboardInterrupt` propagating through the try block?
    Yes. `finally` runs for every exit path and does not distinguish exception types, so `BaseException` subclasses such as `KeyboardInterrupt` and `SystemExit` are discarded exactly like a `ValueError`. That is one reason the pattern is dangerous in long-running code: a process can appear to ignore Ctrl-C, and an intended `sys.exit()` can be neutralised, with nothing in the logs.
  • How do you make Python 3.14's warning about this fail a build instead of printing?
    Promote it: run with `python -W error::SyntaxWarning`, or set `PYTHONWARNINGS=error::SyntaxWarning` in the CI environment. Because PEP 765's warning is emitted by the compiler, the promoted form is a `SyntaxError` raised while the module is compiled, so the failure is immediate and covers code paths tests never execute. Most linters have flagged the pattern for years and can gate it too.

Think of finally as the last person to speak before the door closes. Whatever the function was carrying out — a value or an error — that last word replaces it, and the caller only ever hears the last word.

saying these in an interview costs you the question

  • Says the try block's return wins over the finally block
  • Claims finally cannot change what a function returns
  • Believes the exception still propagates after a return in finally
  • Thinks rebinding the local in finally changes the returned value
  • Treats return in finally as a normal way to force a default
  • Assumes Python 3.14 rejects return inside finally outright

context

open as a page

Why does a `break` or `continue` inside a Python `finally` block silently swallow an exception?

level: middleimportance: should knowfreq 30%

basics

~20 s

A finally block runs while the exception is still in flight and re-raises it only when the block ends normally. A break or continue leaves the block early, so the held exception is dropped and the loop simply carries on.

open as a page

In a log-ingest worker, a `finally` block's flush raises while a parse error is already propagating — which error reaches the caller?

level: seniorimportance: should knowfreq 27%

basics

~20 s

The cleanup error wins. The exception raised inside finally propagates, and the parse error survives only as its context, printed under "During handling of the above exception". Upstream handlers keyed on the original type stop firing.

open as a page