skip to content

Why does Python 3.14 warn about a `break` that leaves a `finally` block?

level: seniorimportance: nice to knowfreq 15%

answer

  1. a jump inside the cleanup block
  2. what is still pending while it runs?
  3. the exception is dropped, not re-raised
  4. PEP 765, a compile-time SyntaxWarning

basics

~20 s

Because leaving a finally block by break, continue or return silently discards whatever was in flight — including an exception that was propagating out of the try. Python 3.14 (PEP 765) emits a SyntaxWarning at compile time for that shape.

solid answer

~50 s

A `finally` block runs while an exception may still be propagating. If control leaves that block by a jump — `break`, `continue` or `return` — the pending exception is abandoned rather than re-raised, so a genuine failure vanishes and the code reads as if nothing went wrong. The same applies to a `return` in `finally` overriding a value the `try` already returned. PEP 765 made this visible in Python 3.14: the compiler emits a `SyntaxWarning` naming the offending statement when it would exit a `finally` block, at compile time, so it appears when the module is imported rather than when the bad path is hit. It is a warning, not an error, and existing code keeps running; a future release may tighten it. The fix is to move the jump out of `finally`, or to handle the exception explicitly with `except`.

code

python · 16 lines
python
import warnings

source = (
    "for n in range(3):\n"
    "    try:\n"
    "        raise ValueError('boom')\n"
    "    finally:\n"
    "        break\n"
)
with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    code = compile(source, "<demo>", "exec")
for w in caught:
    print(w.category.__name__, w.message)
exec(code)
print("no exception escaped")

go deeper

for a junior

Just know the rule of thumb: put cleanup in finally and nothing else. A break or return written there can make a real error disappear without a trace.

for a middle

Explain the mechanism — the exception is held pending while finally runs and is re-raised only if the block ends normally, so a jump out of it discards the exception entirely.

for a senior

Be able to place it in time: Python 3.14 and PEP 765 added a compile-time SyntaxWarning, continue there was a SyntaxError before 3.8, and the right fix is an explicit except rather than a jump.

for a principal

Decide the policy: whether this warning is escalated to an error in CI, how a codebase with legacy occurrences is migrated ahead of a possible future tightening, and how silent exception-swallowing is caught in review generally.

This is a small, sharp corner of Python's control flow, and it became visible in Python 3.14. ### What the construct does A `finally` block runs on every route out of its `try` statement, and one of those routes is "an exception is propagating". While the `finally` body executes, that exception is held pending; when the body finishes normally, the interpreter re-raises it and propagation continues. If instead control leaves the `finally` block by a jump — `break`, `continue` or `return` — the jump wins. The pending exception is discarded, never re-raised, and never logged. The loop simply exits, or the function simply returns, as though the `try` body had succeeded. The same swallowing applies to values, not just exceptions: a `return` inside `finally` overrides a `return` already staged by the `try` suite, so the function quietly gives back the wrong result. ### Why it is worth a warning The construct almost always means the author misread `finally` as "the end of the loop body" rather than "cleanup on every exit route". The failure mode is the worst kind — silence. A webhook receiver reconciling a 6,800-row batch might validate each row inside a `try` and do its per-row bookkeeping inside `finally`; if someone adds a `break` to that `finally` to stop after the first anomaly, the `ValueError` raised by the drift check is discarded on the way out, and the batch is reported clean while a rounding drift sits in the data. Nothing in the logs points at the `finally`. It is also unreviewable at a glance: the `raise` and the swallow are in different suites of the same statement, and the swallow is invisible — there is no `except`, no `pass`, nothing that reads as "ignore errors". ### What 3.14 actually does PEP 765, *Disallow return/break/continue that exit a finally block*, landed in Python 3.14. The compiler now emits a `SyntaxWarning` whose message names the statement and the block, for example `'break' in a 'finally' block`. Two details matter operationally: * It is emitted at **compile time**, so it surfaces when the module is compiled or imported, not when the bad path is finally exercised. That is the good news — you learn about it on first import rather than in production. * It is a **warning**, so it does not change behaviour and does not break existing code. The PEP's stated direction is that it may become an error in a later release, so treat a warning today as a defect to fix rather than noise to filter. Because it is a warning, the usual controls apply: escalate it to an error for the whole process with the interpreter's `-W error::SyntaxWarning` command-line option, or capture it programmatically around a `compile()` call with `warnings.catch_warnings` and `warnings.simplefilter`. The warning covers only a jump that *exits* the `finally` block. A `break` inside a loop that is itself written inside the `finally` body is fine — it never leaves the block. ### History This corner has moved before. `continue` inside a `finally` block was an outright `SyntaxError` until Python 3.8, when the restriction was lifted for implementation reasons; `break` and `return` were always permitted. Python 3.14 brings all three back under scrutiny — this time as a warning across the board rather than an error for one of them. ### How to fix it Decide what you actually meant. If you meant "stop the loop after cleanup", move the `break` out of the `finally` and put it after the whole `try` statement — the cleanup still runs, and a propagating exception is still free to propagate. If you meant "this error is expected and should end the loop", say so explicitly with an `except` clause that catches the specific exception, does what it needs, and then breaks from the `except` suite: leaving an `except` suite by `break` is ordinary control flow with nothing pending, and it is not warned about. If you meant "return this value no matter what", assign it in `finally` and return it after the statement. ### Answering well Name the swallow first — the pending exception is discarded — then the 3.14 warning and that it is a compile-time `SyntaxWarning` rather than an error. Mentioning that the direction of travel is toward an error, and that the right fix is `except` rather than a jump in `finally`, marks the answer as coming from someone who has actually read a release note.

  • What exactly is lost when a `return` exits a `finally` block?
    Whatever was in flight. If an exception was propagating out of the `try`, it is discarded instead of re-raised, so the failure disappears with no traceback. If the `try` suite had already staged a return value, the `finally` return overrides it and the caller silently gets the wrong result. Both are invisible at the call site, which is why the construct is worth a warning rather than a style note.
  • How would you make Python 3.14's `SyntaxWarning` fail a build instead of printing?
    Escalate the warning to an error with the interpreter's `-W error::SyntaxWarning` option, so importing an offending module raises instead of printing. Because the warning is emitted at compile time, compiling the package's sources under that setting is enough to catch every occurrence without executing anything. Programmatically, wrap a `compile()` call in `warnings.catch_warnings` with `warnings.simplefilter` set to `error`.
  • Is every `break` written inside a `finally` block warned about?
    No — only one that actually exits the `finally` block. A `break` belonging to a loop written entirely inside the `finally` body stays within the block, nothing is abandoned on the way out, and no warning is emitted. The rule is about control escaping the block while an exception or a staged return value may still be pending, not about the keyword appearing there.

saying these in an interview costs you the question

  • Thinks a jump out of finally re-raises the pending exception
  • Calls the 3.14 diagnostic a SyntaxError rather than a warning
  • Says the warning appears only when the path runs
  • Uses break in finally to force a loop exit
  • Believes a return in finally cannot override the try's value
  • Suppresses the warning instead of fixing the code

context