2025, Dec 02 11:00
Don't return in finally: avoiding swallowed exceptions in Python context managers
Why returning in a finally block suppresses exceptions in Python context managers, how it breaks exception propagation, and the safe pattern to fix it.
Python’s context managers are often used as the cleanest way to guarantee teardown, but they can behave in surprising ways if you mix them with certain control-flow choices. A common pitfall is putting a return inside a finally block. The effect is subtle and easy to miss: the exception you expected to see never makes it out.
Reproducing the issue
Consider a context manager that always executes its cleanup code in finally and then returns. The surrounding code raises and re-raises an exception. You might expect it to propagate. It doesn’t.
from contextlib import contextmanager
@contextmanager
def run_ctx():
try:
print("enter ctx")
yield
finally:
print("leave ctx")
return # masks any exception raised inside the with-body
with run_ctx():
try:
raise RuntimeError("ASDF")
except Exception as err:
print(f"caught {err}; re-raising")
raise err
print("still running")
The code completes execution after the with block instead of stopping on the RuntimeError. The exception is swallowed.
What’s really happening
This behavior is not specific to contextlib.contextmanager. A plain function shows the same effect, which points to finally itself rather than the context manager machinery.
def sample():
try:
raise Exception("Oh no")
except:
raise
finally:
return
sample()
A finally block is always invoked. If it executes a return, that return overrides any pending raise or return from the try or except parts. The function simply returns a value (None here), and control flow continues as if nothing exceptional had happened.
You can think of it in the most minimal form:
try:
raise Exception("oh no")
finally:
return
# the return above prevents the exception from propagating further
Once execution hits return in finally, nothing else can happen in that call frame. The previously pending exception is discarded.
Fixing the flow
The practical rule is simple: don’t return in a finally block. Let finally finish its cleanup, and allow the normal control flow to resume. If an exception was raised, it will propagate; if not, the function or context manager will proceed as usual.
from contextlib import contextmanager
@contextmanager
def run_ctx():
try:
print("enter ctx")
yield
finally:
print("leave ctx") # no return here
with run_ctx():
try:
raise RuntimeError("ASDF")
except Exception as err:
print(f"caught {err}; re-raising")
raise err
Cleanup still runs, and the exception is no longer suppressed. If there’s no exception, the with block completes normally.
Why you should care
Silent failure modes are some of the hardest to track in production. A stray return inside finally can mute exceptions that your tests and logging depend on, making root-cause analysis much harder. Because the effect isn’t limited to context managers, the same mistake can slip into any function that uses try/except/finally, compounding the surprise.
Takeaways
Use finally for cleanup, logging, or other unconditional actions, and avoid returning from it. The runtime guarantees that finally will run; once it completes, normal control flow resumes, including proper exception propagation. Keeping return statements out of finally preserves that behavior and prevents accidental exception swallowing.