Python's finally Return Trap: The Exception That Vanishes

2026-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.

The Bug

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):

For safe_divide(10, 0), it's worse:

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.

The Fix

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.

Key Takeaway: A return inside finally silently overrides both return values and active exceptions — use finally only for cleanup, never for control flow.

All newsletters