try / except / else / finally
The full try/except/else/finally statement is Python's structured way to attempt risky code, handle specific failures, run success-only logic, and guarantee cleanup — all in one construct.
Exceptions describe what can go wrong; the try statement is how you structure code to actually respond. Its four clauses — try, except, else, and finally — each have a distinct, well-defined role, and together they let you separate 'the risky operation', 'what to do if it fails', 'what to do only if it succeeds', and 'what must always happen' into clearly labeled blocks.
The Basic try / except
Code inside try is executed normally. If an exception occurs, execution jumps immediately to a matching except clause instead of continuing the rest of the try block. If no exception occurs, every except clause is skipped entirely.
try:
number = int(input('Enter an integer: '))
except ValueError:
print('That was not a valid integer.')Catching Multiple Exception Types
A single except clause can list several exception types in a tuple to handle them the same way, or you can stack multiple except clauses to handle different types differently. Python checks except clauses top to bottom and uses the first one that matches.
Order matters when exception types overlap: a broader parent class listed before a more specific subclass will catch it first, so more specific except clauses should generally come before more general ones.
try:
value = data[key] / divisor
except (KeyError, ZeroDivisionError) as e:
print(f'lookup or math problem: {e}')
except TypeError as e:
print(f'wrong type involved: {e}')Capturing the Exception Object with as
except ExceptionType as e: binds the caught exception instance to e, giving access to its message and any custom attributes. That binding only exists inside the except block; Python automatically deletes it when the block ends.
try:
1 / 0
except ZeroDivisionError as e:
print(type(e).__name__, ':', e)
print(e.args)The else Clause: Success-Only Code
Code in else runs only if the try block completed without raising any exception. This lets you keep the try block minimal — containing only the risky operation — and move code that depends on its success out, so it isn't accidentally covered by the except clauses (which are meant to catch failures in the try block, not in follow-up logic).
Without else, putting contents = f.read() inside the try block would mean an exception from read() itself gets caught by the same except FileNotFoundError — likely not intended, since that clause was written for the open() call.
try:
f = open('data.txt')
except FileNotFoundError:
print('file missing')
else:
contents = f.read() # only runs if open() succeeded
f.close()
print(contents)The finally Clause: Guaranteed Cleanup
Code in finally always runs — whether the try block succeeded, raised a handled exception, raised an unhandled exception, or even hit a return/break/continue that would otherwise exit the block early. It's the right place for cleanup that must happen regardless of outcome: closing files, releasing locks, disconnecting resources.
'file closed' prints whether the function returns a line successfully or hits the except branch — finally runs on every path out of the try.
For resource cleanup specifically, a with statement (context manager) is usually preferred over manual try/finally, since __exit__ handles it automatically and more concisely — but try/finally remains the general-purpose mechanism finally is built on.
def read_first_line(path: str) -> str | None:
f = None
try:
f = open(path)
return f.readline()
except FileNotFoundError:
return None
finally:
if f is not None:
f.close()
print('file closed')All Four Clauses Together
The full order is always try, then any number of except clauses, then an optional single else, then an optional single finally. Execution flows: try body runs; on exception, the matching except runs (else is skipped); on no exception, else runs (if present); finally always runs last, regardless of what happened above.
def process(value: str) -> None:
try:
number = int(value)
except ValueError:
print('not a number')
else:
print(f'doubled: {number * 2}')
finally:
print('processing attempt finished')
process('21')
# doubled: 42
# processing attempt finished
process('abc')
# not a number
# processing attempt finishedfinally and return: An Important Edge Case
If both a try (or except) block and its finally block contain a return, the finally block's return silently overrides the earlier one — the earlier return's value is discarded. This is a genuine, easy-to-miss trap; avoid returning from finally except deliberately.
def confusing() -> str:
try:
return 'from try'
finally:
return 'from finally' # this wins
print(confusing()) # 'from finally'Exceptions Inside except, else, or finally
Any of these clauses can itself raise a new exception. An exception raised inside an except block is not caught by that same except (or any preceding one) — it propagates normally, chained to the original via __context__ as covered in the Exceptions topic.
Best Practices
Keep the try block narrow — only the statement(s) that can actually raise the exception you're handling — and use else for logic that should run only after success. Prefer specific exception types over broad ones. Use finally (or, better, a context manager) for cleanup, and be deliberate if a return ever appears inside one.
Key Takeaways
try contains risky code; except handles specific matching exception types; else runs only if try succeeded; finally always runs.
Multiple except clauses are checked top to bottom; the first matching type wins.
else keeps success-only logic out of the try block, so it isn't accidentally caught by the same except handlers.
finally runs on every path out of try/except, including return, break, and unhandled exceptions — making it the right place for guaranteed cleanup.
A return inside finally silently overrides a return from try or except — a subtle trap worth avoiding.
try: f = open(path) contents = f.read() process(contents) except FileNotFoundError: print('missing') try: f = open(path) except FileNotFoundError: print('missing') else: contents = f.read() process(contents) f.close()
Putting everything in the try block means an unrelated error from read() or process() gets silently caught by the except meant for a missing file; else scopes success-only code correctly.
def f(): try: return 1 finally: return 2 # silently discards the try's return value def f(): try: return 1 finally: cleanup() # no return here
A return statement inside finally always wins, silently discarding any return (or exception) from the try/except above it — almost never the intended behavior.
What is the execution order of try, except, else, and finally when the try block succeeds with no exception?
The try block runs, all except clauses are skipped, the else block runs (if present), and then finally runs (if present).
Why does the else clause exist, given that code could just be placed at the end of the try block instead?
Placing success-only code inside try risks it being accidentally caught by an except clause meant for a different, earlier statement; else scopes it to run only after the try block has fully succeeded, outside except's reach.
Under what circumstances does a finally block NOT run?
Essentially never during normal control flow — it runs after try/except whether the try succeeded, raised a handled exception, raised an unhandled one, or hit return/break/continue; only catastrophic interpreter termination (e.g. the process being killed) skips it.
If both a try block and its finally block contain a return statement, which value does the function actually return?
The finally block's return value — it silently overrides the try block's return.
When multiple except clauses could match the same exception, which one runs?
The first one, checked top to bottom, whose exception type matches (including via inheritance).
What does except (TypeError, ValueError) as e: do differently from having two separate except clauses for TypeError and ValueError?
It handles both types with the exact same block of code, using a single shared handler, rather than duplicating the handling logic across two clauses.
Why is try/finally still relevant even though with statements are usually preferred for resource cleanup?
with (context managers) is a more concise, purpose-built layer for resource acquisition/release, but it's built on the same underlying guarantee that try/finally provides for cleanup on every exit path, and try/finally remains the general mechanism for cases without a context manager.
Write a function safe_divide(a, b) that returns a/b, prints 'cannot divide by zero' and returns None on ZeroDivisionError, prints 'ok' via else on success, and always prints 'done' via finally.
def safe_divide(a: float, b: float) -> float | None: try: result = a / b except ZeroDivisionError: print('cannot divide by zero') return None else: print('ok') return result finally: print('done') safe_divide(10, 2) prints 'ok' then 'done', returns 5.0. safe_divide(10, 0) prints 'cannot divide by zero' then 'done' (finally still runs even though except already executed a return), returns None.
What gets printed, and why does 'else' not appear?
It prints 'caught' then 'finally'. else is skipped because the try block raised an exception (else only runs when try completes with no exception at all); finally still runs regardless.
Predict the output: a function has try: return 'A' followed by finally: print('cleanup') with no return in finally. What prints and what does the function return?
'cleanup' prints (finally always runs before the function actually returns), and the function returns 'A' — since finally here has no return of its own, it doesn't override the try block's return value; it only runs its side effect (the print) first.
def f():
try:
raise ValueError('bad')
except ValueError:
print('caught')
else:
print('else')
finally:
print('finally')
f()