finally Return Trap: The Exception That Vanishes2026-05-03
A teammate writes a utility that divides two numbers and guarantees a "safe" default return value. It should return the result on success, or -1 as a sentinel value on failure. Simple enough:
def safe_divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
print("ERROR: division by zero!")
raise
finally:
return -1
# --- Test harness ---
print(safe_divide(10, 2)) # expect 5.0
print(safe_divide(10, 0)) # expect ZeroDivisionError raised
print(safe_divide(7, 3)) # expect 2.333...
You run it and get:
-1
ERROR: division by zero!
-1
-1
Every single call returns -1. The successful divisions are wrong. And the division-by-zero case prints the error message but never actually raises the exception. It silently returns -1 instead. No traceback, no crash, nothing.
A return statement inside a finally block unconditionally overrides any return value from the try or except blocks — and, critically, it silently suppresses any active exception, including one that was explicitly re-raised with raise.
Here's the execution flow for safe_divide(10, 2):
try block computes result = 5.0 and executes return result.finally block.finally block executes return -1, which replaces the pending return 5.0.-1.For safe_divide(10, 0), it's worse:
except block catches the error, prints the message, and calls raise to re-raise.ZeroDivisionError is now the "active" exception propagating up the stack.finally block runs and hits return -1.return statement cancels the in-flight exception entirely. It's gone.-1 as if nothing went wrong.This is specified behavior in the Python language reference: "If a finally clause includes a return statement, the returned value will be the one from the finally clause, not the value from the try clause's return statement." And exceptions are discarded if finally returns.
This makes the bug particularly insidious. The code looks like it has belt-and-suspenders error handling — a raise to propagate the error, plus a finally for cleanup. But the finally return turns the entire error-handling strategy into a black hole.
Never use return inside a finally block. Use finally exclusively for cleanup (closing files, releasing locks). Put your return values in try and except:
def safe_divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
print("ERROR: division by zero!")
raise
Or, if you genuinely want a sentinel fallback instead of re-raising:
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
print("ERROR: division by zero!")
return -1
Linters like Pylint (W0150: return-in-finally) and Ruff (B012) will flag this. Enable them. This same behavior exists in JavaScript and Java — a return in finally silently swallows exceptions in all three languages.
return inside finally silently overrides both return values and active exceptions — use finally only for cleanup, never for control flow.
