The execution order of exception handling blocks is non-obvious and regularly tested in senior interviews
Most developers use try/except regularly. Fewer can predict the exact execution order when you combine try, except, else, and finally in a single block, especially when return statements or nested exceptions are involved.
The Complete Structure
try:
# code that might raise an exception
except SomeException:
# runs if SomeException is raised in try
except AnotherException:
# runs if AnotherException is raised in try
else:
# runs if NO exception was raised in try
finally:
# ALWAYS runs, exception or not
The else block is the least known. It runs only when the try block completes without raising any exception. It is useful for code that should run only on success but is not part of the operation you are protecting.
Basic Execution Order Traces
Case 1 — No exception:
try:
x = 1 + 1
except ValueError:
print("value error")
else:
print("else block")
finally:
print("finally block")
Output:
else block
finally block
The try block succeeds. The except block is skipped. The else block runs. The finally block runs.
Case 2 — Exception raised and caught:
try:
x = int("not a number")
except ValueError:
print("value error caught")
else:
print("else block")
finally:
print("finally block")
Output:
value error caught
finally block
The try block raises ValueError. The except block catches it. The else block is skipped because an exception occurred. The finally block runs.
Case 3 — Exception not caught:
try:
x = 1 / 0
except ValueError:
print("value error")
else:
print("else block")
finally:
print("finally block")
Output:
finally block
ZeroDivisionError: division by zero
The ZeroDivisionError is not caught by the except clause. The finally block still runs before the exception propagates.
The return Inside finally
def tricky():
try:
return "from try"
finally:
return "from finally"
print(tricky())
Output: from finally
This is a genuine interview trap. The finally block executes even when there is a return in the try block. The finally return overrides the try return. This behavior is technically correct but widely considered bad practice.
Exception Chaining
try:
try:
x = 1 / 0
except ZeroDivisionError:
raise ValueError("converted error") from None
except ValueError as e:
print(f"caught: {e}")
print(f"cause: {e.__cause__}")
Output:
caught: converted error
cause: None
raise X from None suppresses the exception chain, meaning the original ZeroDivisionError is not preserved as the cause. Without from None, the original exception would be accessible through e.__cause__.
The Interview Question
def process(data):
result = []
for item in data:
try:
result.append(10 / item)
except ZeroDivisionError:
result.append(None)
else:
print(f"processed {item}")
finally:
print(f"done with {item}")
return result
output = process([2, 0, 5])
print(output)
Trace each iteration:
item=2: 10/2 = 5, appended. else runs: "processed 2". finally runs: "done with 2". item=0: ZeroDivisionError. except runs: appends None. else is skipped. finally runs: "done with 0". item=5: 10/5 = 2, appended. else runs: "processed 5". finally runs: "done with 5".
Output:
processed 2
done with 2
done with 0
processed 5
done with 5
[5.0, None, 2.0]
Practice exception handling trace problems at pycodeit.com.
Top comments (0)