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
elseruns only when there is no exception.finallyalways runs regardless of whether an exception occurred.exceptclauses 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}")