Skip to main content
Advertisement

try / except / else / finally — Understanding Exception Flow

When writing programs, you inevitably encounter error situations. When a file doesn't exist, a network disconnects, or you try to convert a non-numeric value, Python raises an Exception. The try-except construct is the core tool for safely catching and handling these exceptions.


Exception Flow Diagram

Execute try block

├─ No exception ──▶ Execute else block ──▶ Execute finally block ──▶ Normal exit

└─ Exception raised ──▶ Search for matching except

├─ Match found ──▶ Execute except block ──▶ Execute finally ──▶ Continue

└─ No match ──▶ Propagate exception up the call stack

Key Rules

  • else runs only when there is no exception.
  • finally always runs regardless of whether an exception occurred.
  • except clauses are checked top to bottom in order.

Basic Structure

try:
# Code that may raise an exception
result = 10 / 0
except ZeroDivisionError:
# Handle specific exception
print("Cannot divide by zero.")

Multiple except Clauses — Order Matters

To handle multiple exceptions separately, write multiple except clauses. More specific exceptions first, more general exceptions last.

def parse_number(value: str) -> float:
try:
return float(value)
except ValueError:
print(f"'{value}' is not a valid number.")
return 0.0
except TypeError:
print(f"String required. Received type: {type(value).__name__}")
return 0.0
except Exception as e:
# Safety net for unexpected exceptions
print(f"Unexpected error: {e}")
return 0.0

print(parse_number("3.14")) # 3.14
print(parse_number("abc")) # 0.0
print(parse_number(None)) # 0.0

Incorrect ordering — a pattern to always avoid

# Bad example: if Exception comes before ValueError,
# ValueError is dead code and will never execute
try:
float("abc")
except Exception: # Already catches all exceptions
print("General error")
except ValueError: # Never reached
print("Value error") # This line never executes

The as Keyword — Referencing the Exception Object

Using the as keyword to bind the exception object to a variable lets you access detailed information.

try:
numbers = [1, 2, 3]
print(numbers[10])
except IndexError as e:
print(f"Exception type: {type(e).__name__}")
print(f"Exception message: {e}")
print(f"Exception args: {e.args}")
Exception type: IndexError
Exception message: list index out of range
Exception args: ('list index out of range',)

else Clause — Runs When No Exception Occurs

The else block runs only when the try block succeeds without an exception. It cleanly separates success-path logic from error-handling logic.

def divide(a: float, b: float) -> float | None:
try:
result = a / b
except ZeroDivisionError:
print("Cannot divide by zero.")
return None
else:
# Only runs if no exception was raised
print(f"Success: {a} / {b} = {result}")
return result

divide(10, 2) # Success: 10 / 2 = 5.0
divide(10, 0) # Cannot divide by zero.

Why use else: Putting too much code inside the try block can accidentally catch unintended exceptions. Using else to separate success handling minimizes the scope of exception handling.


finally Clause — Always Runs (Resource Cleanup)

finally always runs regardless of whether an exception occurred. Use it for resource cleanup such as files, network connections, or database connections.

def read_file(path: str) -> str:
file = None
try:
file = open(path, "r", encoding="utf-8")
return file.read()
except FileNotFoundError:
print(f"File not found: {path}")
return ""
except PermissionError:
print(f"No permission to read file: {path}")
return ""
finally:
# Close the file regardless of whether an exception occurred
if file is not None:
file.close()
print("File handle released")

Warning: Using return inside finally will suppress any return values or exceptions from try/except.

def risky():
try:
raise ValueError("Error")
finally:
return "returned from finally" # The exception is swallowed!

print(risky()) # "returned from finally" — exception disappears

Full Structure — Combining try / except / else / finally

import logging

logger = logging.getLogger(__name__)

def load_config(path: str) -> dict:
"""Reads a config file and returns it as a dictionary."""
file = None
try:
file = open(path, "r", encoding="utf-8")
import json
data = json.load(file)
except FileNotFoundError:
logger.error("Config file not found: %s", path)
return {}
except json.JSONDecodeError as e:
logger.error("JSON parse failure: %s (line %d)", e.msg, e.lineno)
return {}
else:
logger.info("Config file loaded successfully: %d keys", len(data))
return data
finally:
if file is not None:
file.close()

Practical Example 1 — File Processing Pattern

from pathlib import Path

def safe_read_lines(path: str | Path) -> list[str]:
"""
Reads a file and returns a list of lines.
Returns an empty list on error.
"""
try:
with open(path, "r", encoding="utf-8") as f:
lines = f.readlines()
except FileNotFoundError:
print(f"[Warning] File not found: {path}")
return []
except UnicodeDecodeError:
# Retry with CP949 (Windows Korean) if UTF-8 fails
try:
with open(path, "r", encoding="cp949") as f:
lines = f.readlines()
except Exception as e:
print(f"[Error] Encoding failure: {e}")
return []
else:
return [line.rstrip("\n") for line in lines]

return [line.rstrip("\n") for line in lines]

Practical Example 2 — Database Connection Pattern

import sqlite3
from typing import Any

def execute_query(
db_path: str,
query: str,
params: tuple = ()
) -> list[dict[str, Any]]:
"""
Executes a SQLite query and returns the results.
"""
conn = None
try:
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(query, params)
rows = cursor.fetchall()
except sqlite3.OperationalError as e:
print(f"SQL error: {e}")
return []
except sqlite3.DatabaseError as e:
print(f"DB error: {e}")
return []
else:
return [dict(row) for row in rows]
finally:
if conn:
conn.close()

# Usage example
results = execute_query(
"app.db",
"SELECT * FROM users WHERE active = ?",
(1,)
)

Practical Example 3 — Retry Pattern

import time
from typing import Callable, TypeVar

T = TypeVar("T")

def retry(
func: Callable[[], T],
max_attempts: int = 3,
delay: float = 1.0,
exceptions: tuple[type[Exception], ...] = (Exception,)
) -> T:
"""
Retries the function up to max_attempts times.
"""
last_exception: Exception | None = None

for attempt in range(1, max_attempts + 1):
try:
return func()
except exceptions as e:
last_exception = e
print(f"Attempt {attempt}/{max_attempts} failed: {e}")
if attempt < max_attempts:
time.sleep(delay)
else:
break # Stop retrying on success

raise RuntimeError(
f"All {max_attempts} attempts failed"
) from last_exception

# Usage example
import random

def flaky_operation() -> str:
if random.random() < 0.7: # 70% chance of failure
raise ConnectionError("Network error")
return "Success"

result = retry(flaky_operation, max_attempts=5, delay=0.1)
print(result)

Expert Tips

Tip 1 — Minimize the try block

# Bad: try block is too wide
try:
data = load_data()
processed = transform(data)
result = save(processed)
except Exception:
pass # No idea where it failed

# Good: handle each step separately
data = load_data() # Handle exceptions separately
processed = transform(data) # Handle exceptions separately
result = save(processed) # Handle exceptions separately

Tip 2 — Never use bare except

# Absolutely forbidden: catches KeyboardInterrupt, SystemExit too
try:
do_something()
except: # bare except
pass

# Correct: explicitly name Exception
try:
do_something()
except Exception:
pass

Tip 3 — Don't swallow exceptions silently

# Bad: silently ignoring exceptions makes bug tracing impossible
try:
result = compute()
except ValueError:
pass # Silently ignored

# Good: at least log it
import logging
try:
result = compute()
except ValueError as e:
logging.warning("compute() failed, using default: %s", e)
result = default_value

Tip 4 — Replace the finally pattern with a with statement

# Old pattern: release resources with finally
file = None
try:
file = open("data.txt")
content = file.read()
finally:
if file:
file.close()

# Using with is much more concise and safe
with open("data.txt") as file:
content = file.read()

Tip 5 — Python 3.11+ ExceptionGroup

Python 3.11 introduced ExceptionGroup and the except* syntax for handling multiple exceptions simultaneously.

# Python 3.11+
try:
raise ExceptionGroup("multiple errors", [
ValueError("value error"),
TypeError("type error"),
])
except* ValueError as eg:
print(f"ValueError group: {eg.exceptions}")
except* TypeError as eg:
print(f"TypeError group: {eg.exceptions}")
Advertisement