Back to Blog
Python

Python Try Except Else Finally: Execution Order

Understand exactly when the else and finally clauses run in Python's try/except statement, and how to use them for precise error handling.

exception handlingpython control flowtry except finallyerror handlingpython syntax
Flow diagram showing the execution order of try, except, else, and finally clauses in a Python exception handling block

Python's try statement can use up to four clause types: try, except, else, and finally. Most developers use try and except regularly, but the else and finally clauses are frequently misunderstood. The key question is: when exactly does each clause run, and what guarantees does the language actually provide?

How the Four Clauses Execute in Order

The execution order is deterministic. When a try block starts, Python evaluates the code inside it. If the try block completes normally without raising an exception, the else clause runs immediately after it. The finally clause runs last, whether an exception was raised, handled, or left unhandled.

def read_config(path): try: with open(path) as f: config_text = f.read() except FileNotFoundError: return "{}" else: print("config loaded successfully") return config_text finally: print("read_config finished")

If the file exists, the order is: try body, then else, then finally. If the file is missing, the order is: try body, then except, then finally. The else clause never runs when the try block raised an exception, even if that exception was handled by except.

One edge case: if a return, break, or continue statement exits the try block, the else clause is skipped because the block did not complete normally. The finally clause, if present, still runs.

What the else Clause Actually Does

The else clause runs only when the try block completes normally without raising an exception. This is useful for code that should execute only on success but should not be inside the try block itself.

Why does that distinction matter? Code inside the try block is protected by the except clauses. If you put success-only logic inside try, an exception raised by that logic would also be caught by the same except handler. That can hide bugs. The else clause is outside the protection of except, so an exception raised there propagates normally.

try: result = parse_user_input(raw) except ValueError: result = None else: validate_result(result) # exceptions here are NOT caught above

Here, validate_result runs only after successful parsing. If it raises, the except ValueError handler does not catch it, because the else clause is not part of the protected region. This separation keeps the error handling scope precise.

Why finally Runs Even When You Return

The finally clause is executed whenever the try statement is left, no matter how it is left. That includes normal completion, a handled exception, an unhandled exception, a return statement, a break, or a continue.

def acquire_lock(): try: lock.acquire() return perform_operation() finally: lock.release()

Even if perform_operation raises or returns early, lock.release() runs before the function actually returns to its caller. This is the primary reason finally exists: resource cleanup that must happen unconditionally.

One subtlety: if the finally block itself raises an exception, that exception replaces whatever was being returned or raised. This can mask the original error. Keep cleanup code in finally simple and avoid operations that are likely to fail, such as network calls or complex parsing.

Common Mistakes with else and finally

A frequent mistake is using else when the intent is simply "run this after the try block." The else clause is not a general post-try hook; it is a success-only hook. If you need code to run regardless of success or failure, use finally.

Another common mistake is placing cleanup logic in except rather than finally. If the try block succeeds, the except block never runs, and cleanup is skipped. The same cleanup must then be duplicated in the success path, which is error-prone.

# Wrong: cleanup only happens on failure conn = connect() try: data = query(conn) except Exception: conn.close() raise # Right: cleanup always happens after the connection is made conn = connect() try: data = query(conn) finally: conn.close()

The second version guarantees conn.close() runs once a connection has been established, whether the query succeeds or raises an exception. If connect() itself raises, there is no connection to close.

Nested try Blocks and Exception Propagation

When try blocks are nested, the else and finally clauses of the inner block run before the outer block's clauses. Exception propagation follows the same rule: an unhandled exception from an inner else clause moves to the nearest enclosing try/except.

try: try: risky_call() except ValueError: handle_value_error() else: success_hook() finally: outer_cleanup()

If risky_call() raises ValueError, the inner except handles it, then the outer finally runs. If success_hook() raises a different exception, it is not caught by the inner except ValueError, so it propagates to the outer try, which has no except here, and then to the caller. The outer finally still runs before the exception leaves the function.

Performance and Maintainability Tradeoffs

The try/except/else/finally structure has negligible runtime cost when no exception is raised. Python's exception handling is designed for the common case where exceptions do not occur. The measurable cost appears when an exception is raised and handled; raising and catching an exception is significantly more expensive than a normal branch.

For maintainability, the else clause reduces the surface area of except. Keeping success-only logic out of the protected region means the except handler only deals with failures from the specific operation it was written for. This makes the error-handling contract easier to reason about during code review.

A practical guideline: use else when the success path contains logic that should not be accidentally caught by the same except. Use finally for any resource that must be released. Avoid putting large amounts of logic in finally, because an exception there replaces the original outcome and complicates debugging.

The full try/except/else/finally structure is most valuable in code that performs resource acquisition, network I/O, or multi-step parsing where the success path and failure path are genuinely different. For simple operations where the try block is a single function call, the else clause often adds little value and can be omitted.

Python Try Except Else Finally: Execution Order and Examples | RYUSLOG DEV