Exception Hierarchy
BaseException
โโโ SystemExit
โโโ KeyboardInterrupt
โโโ Exception
โโโ ArithmeticError
โ โโโ ZeroDivisionError
โ โโโ OverflowError
โโโ LookupError
โ โโโ IndexError
โ โโโ KeyError
โโโ ValueError
โโโ TypeError
โโโ AttributeError
โโโ NameError
โโโ OSError
โ โโโ FileNotFoundError
โ โโโ PermissionError
โโโ RuntimeError
try / except / else / finally
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero")
except (TypeError, ValueError) as e:
print(f"Type or Value error: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
else:
# runs ONLY if no exception was raised
print(f"Result: {result}")
finally:
# ALWAYS runs (cleanup)
print("Done")
Catching and Re-raising
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
raise ValueError(f"Cannot divide {a} by zero") from None
# Exception chaining:
try:
open("missing.txt")
except FileNotFoundError as e:
raise RuntimeError("Config file missing") from e
Custom Exceptions
class ValidationError(ValueError):
"""Raised when validation fails"""
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"Validation error on '{field}': {message}")
class AgeError(ValidationError):
pass
def validate_age(age):
if not isinstance(age, int):
raise ValidationError("age", "must be an integer")
if age < 0 or age > 150:
raise AgeError("age", f"{age} is out of valid range [0, 150]")
return age
try:
validate_age(-5)
except AgeError as e:
print(f"Field: {e.field}") # age
print(f"Error: {e.message}") # -5 is out of valid range [0, 150]
Context Managers
The with statement ensures proper resource cleanup.
# File handling
with open("data.txt", "w") as f:
f.write("Hello, World!")
# File automatically closed here, even if an error occurs
# Multiple context managers
with open("input.txt") as infile, open("output.txt", "w") as outfile:
outfile.write(infile.read())
Creating Context Managers with contextlib
from contextlib import contextmanager
import time
@contextmanager
def timer():
start = time.perf_counter()
try:
yield # control passes to the 'with' block here
finally:
elapsed = time.perf_counter() - start
print(f"Elapsed: {elapsed:.4f}s")
with timer():
time.sleep(0.1)
# Elapsed: 0.1004s
@contextmanager
def temporary_directory():
import tempfile, shutil
tmpdir = tempfile.mkdtemp()
try:
yield tmpdir
finally:
shutil.rmtree(tmpdir)
with temporary_directory() as tmpdir:
print(f"Working in: {tmpdir}")
# Directory cleaned up automatically
Context Manager Class
class DatabaseConnection:
def __init__(self, host, port):
self.host = host
self.port = port
self.connection = None
def __enter__(self):
print(f"Connecting to {self.host}:{self.port}")
self.connection = f"connection_{self.host}"
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
print("Closing connection")
self.connection = None
# Return True to suppress exceptions, False to re-raise
return False
with DatabaseConnection("localhost", 5432) as conn:
print(f"Using: {conn}")
# Connecting to localhost:5432
# Using: connection_localhost
# Closing connection
ExceptionGroup โ Python 3.11+
# Handle multiple exceptions simultaneously
try:
raise ExceptionGroup("multiple errors", [
ValueError("bad value"),
TypeError("wrong type"),
])
except* ValueError as eg:
print(f"Value errors: {eg.exceptions}")
except* TypeError as eg:
print(f"Type errors: {eg.exceptions}")
Exercises
Exercise 1: Safe JSON Parser
Write a function that safely parses JSON and returns a default value on failure.
Solution:
import json
def safe_parse(json_str, default=None):
try:
return json.loads(json_str)
except json.JSONDecodeError as e:
print(f"Parse error: {e}")
return default
print(safe_parse('{"key": 42}')) # {'key': 42}
print(safe_parse("invalid json")) # None
Exercise 2: Retry Decorator
Write a decorator that retries a function up to n times on exception.
Solution:
from functools import wraps
import time
def retry(times=3, delay=0, exceptions=(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == times - 1:
raise
if delay:
time.sleep(delay)
print(f"Attempt {attempt+1} failed: {e}")
return wrapper
return decorator
import random
@retry(times=3)
def unreliable():
if random.random() < 0.8:
raise ConnectionError("Network error")
return "Connected!"
Exercise 3: Transaction Context Manager
Implement a simple transaction context manager that rolls back on error.
Solution:
from contextlib import contextmanager
@contextmanager
def transaction(db):
"""Simulates a database transaction"""
try:
print("BEGIN TRANSACTION")
yield db
print("COMMIT")
except Exception as e:
print(f"ROLLBACK (error: {e})")
raise
class MockDB:
def execute(self, sql):
print(f" SQL: {sql}")
db = MockDB()
try:
with transaction(db) as conn:
conn.execute("INSERT INTO users VALUES (1, 'Alice')")
conn.execute("INSERT INTO users VALUES (2, 'Bob')")
# raise ValueError("Simulated error") # uncommenting causes rollback
except ValueError:
pass