try/except/else/finally -- what else is actually for
else runs only if the try block completed with no exception -- its entire purpose is separating "code
that might raise" (in try) from "code that should only run if it didn't" (in else), so the except
block's own errors aren't accidentally caught by a broader net. finally always runs, no exceptions -- and
that "always" is stronger than most people expect: a return inside finally overrides a return from
try, and can even silently swallow an in-flight exception.
def demo(should_raise):
try:
if should_raise:
raise ValueError("boom")
print(" try succeeded")
except ValueError:
print(" except ran")
else:
print(" else ran (only because try succeeded, not part of the except's error handling)")
finally:
print(" finally always runs")
demo(False)
demo(True)
The genuinely dangerous part: return in finally wins over everything, INCLUDING an exception that was actively propagating:
def finally_overrides_return():
try:
return "from try"
finally:
return "from finally" # this wins -- the try's return value is silently discarded
print("finally overriding return:", finally_overrides_return())
def finally_swallows_exception():
try:
raise ValueError("this exception vanishes")
finally:
return "finally's return value" # the ValueError never propagates -- swallowed with no trace
print("finally swallowing an exception entirely:", finally_swallows_exception())