Exception handling is a core mechanism in programming languages for managing unexpected or out-of-normal runtime conditions. Python includes robust tools for catching and resolving exceptions, with the primary try…except syntax family and assertion statements.
The most common structure is try-except-else-finally, with each clause serving a distinct purpose:
try: Contains code that will execute under normal program flowexcept: Executes only when an exception is raised within thetryblockelse: Runs exclusively if thetryblock completes without any exceptionsfinally: Always runs, regardless of whether an exception occurred in thetryblock
Basic Syntax
try:
# Core execution code
pass
except ExceptionType:
# Error handling logic
pass
else:
# Optional success-time code
pass
finally:
# Mandatory cleanup/teardown code
pass
Exception Handling in Functions
A key detail of exception handling in Python functions is how return statements interact with the finally block. Below are common behavior scenarios:
Scenario 1: Except block has return, Finally block has return
def test_exception():
try:
# Trigger division by zero error
divide_result = 5.0 / 0.0
print('Output: I am in try block')
return 0
except:
print('Output: I am in except block')
return 1
else:
print('Output: I am in else block')
return 2
finally:
print('Output: I am in finally block')
return 3
print(f'test: {test_exception()}')
Output:
I am in except block
I am in finally block
test: 3
Eventhough the except block includes a return statement, the finally block still runs. If finally has its own return, that value will override any prior return values from try or except.
Scenario 2: Except block has return, Finally block has no return
def test_exception():
try:
divide_result = 5.0 / 0.0
print('Output: I am in try block')
return 0
except:
print('Output: I am in except block')
return 1
else:
print('Output: I am in else block')
return 2
finally:
print('Output: I am in finally block')
print(f'test: {test_exception()}')
Output:
I am in except block
I am in finally block
test: 1
Here, the finally block runs after the except return, and since it has no return statemant, the value from the except block is used as the function's return value.
Scenario 3: Try block has return, Finally block has return
def test_exception():
try:
# Successful division
divide_result = 5.0 / 1.0
print('Output: I am in try block')
return 0
except:
print('Output: I am in except block')
return 1
else:
print('Output: I am in else block')
return 2
finally:
print('Output: I am in finally block')
return 3
print(f'test: {test_exception()}')
Output:
I am in try block
I am in finally block
test: 3
Even with an early return in the try block, the finally block runs, and its return value takes precedence.
Scenario 4: Try block has return, Finally block has no return
def test_exception():
try:
divide_result = 5.0 / 1.0
print('Output: I am in try block')
return 0
except:
print('Output: I am in except block')
return 1
else:
print('Output: I am in else block')
return 2
finally:
print('Output: I am in finally block')
print(f'test: {test_exception()}')
Output:
I am in try block
I am in finally block
test: 0
The else block is skipped due to the early try return, and the finally block runs, using the try block's return value since finally has no return.
Scenario 5: No exceptions, Try has no return, Finally has no return
def test_exception():
try:
divide_result = 5.0 / 1.0
print('Output: I am in try block')
except:
print('Output: I am in except block')
return 1
else:
print('Output: I am in else block')
return 2
finally:
print('Output: I am in finally block')
print(f'test: {test_exception()}')
Output:
I am in try block
I am in else block
I am in finally block
test: 2
Since there are no exceptions and no early return in try, the else block runs, followed by finally, using the else block's return value.
Key Summary Rules
- The
finallyblock always executes, regardless of whether an exception occurred - The
exceptblock runs only when an exception is raised in thetryblock - The
elseblock runs only if thetryblock completes without exceptions and has no early return - If the
tryblock includes an earlyreturn, theelseblock is skipped entirely - If the
tryblock has no early return and no exceptions, theelseblock executes before thefinallyblock
Practical Application: Grid Grass Spread
This example uses exception handling to handle boundary checks when simulating grass spreading on a grid:
Problem: Given an n x m grid where some cells have grass (
g) and others are empty (.), each month grass spreads to all 4 adjacent cells (up, down, left, right). Calculate the grid state after k months.Input: First line has n and m, next n lines have the grid state, final line has k.
Output: The final grid state after k months.
Sample Input:
4 5
.g...
.....
..g..
.....
2
Sample Output:
gggg.
gggg.
ggggg
.ggg.
# Read grid dimensions
n, m = map(int, input().split())
# Store initial grid state
initial_grid = [list(input().strip()) for _ in range(n)]
# Create a copy to avoid overwriting grass during current iteration
updated_grid = [row.copy() for row in initial_grid]
# Read number of months
months = int(input())
for _ in range(months):
# Reset updated grid for each month
updated_grid = [row.copy() for row in initial_grid]
for i in range(n):
for j in range(m):
if initial_grid[i][j] == 'g':
# Spread grass to adjacent cells, use try-except for boundary checks
# Up
if i > 0:
updated_grid[i-1][j] = 'g'
# Down
try:
updated_grid[i+1][j] = 'g'
except IndexError:
pass
# Left
if j > 0:
updated_grid[i][j-1] = 'g'
# Right
try:
updated_grid[i][j+1] = 'g'
except IndexError:
pass
# Update initial grid for next month's iteration
initial_grid = [row.copy() for row in updated_grid]
# Print final grid
for row in initial_grid:
print(''.join(row))