Exception Handling from Basics to Production
Understand every built‑in exception, try/except/else/finally, raise, custom exceptions, assertions, and production‑grade patterns — with solved and unsolved practice problems.
📌 What you'll learn: Exception handling is not just about try and except. It’s a mindset that separates fragile scripts from production‑ready software. This page covers everything: syntax errors vs exceptions, the full hierarchy, best practices, and how to design your own error strategy.
🎯 Goal: Write Python code that fails gracefully, logs meaningfully, and recovers intelligently — whether you’re a beginner or an experienced developer moving to production systems.
Why Exception Handling? The Big Picture
Exception handling is a programming construct that allows you to catch and respond to runtime errors (exceptions) without crashing the program. It separates the normal flow of code from the error‑recovery logic, making programs more robust and maintainable.
Without exception handling, any unexpected situation – a missing file, a network timeout, invalid user input – would immediately terminate the program. In production, that means lost data, unhappy users, and system downtime. Exception handling gives you control over these scenarios.
- User experience: Show friendly messages instead of raw tracebacks.
- Data integrity: Roll back transactions or save progress before exiting.
- Debugging: Log detailed error information for post‑mortem analysis.
- Resilience: Retry operations, fall back to defaults, or degrade gracefully.
It solves the fragility problem: a single unhandled error should not bring down an entire system. It turns unpredictable failures into manageable events, allowing software to self‑heal or at least shut down cleanly.
In production, exception handling is the backbone of observability and reliability. It enables:
- Centralized error logging and alerting (Sentry, ELK, CloudWatch).
- Automatic retry with backoff (protects against transient failures).
- Circuit breakers that prevent cascading failures.
- Graceful shutdowns that finish in‑flight tasks before termination.
Errors vs Exceptions
Built‑in Exceptions
try/except
else / finally
raise & Custom
Assertions
Production Patterns
1 Errors vs Exceptions – The Foundation
Python distinguishes between syntax errors (parsing problems, your code never runs) and exceptions (runtime errors that occur during execution and can be caught).
# Syntax error – code won't execute
if True
print("Missing colon")
# Runtime exception – ZeroDivisionError
print(10 / 0)
2 Built‑in Exception Hierarchy & Examples
All built‑in exceptions inherit from BaseException. The main branch for everyday errors is Exception. Here are the most common ones every developer must know.
| Exception | Cause | Example Trigger |
|---|---|---|
TypeError | Wrong type | len(42) |
ValueError | Right type, inappropriate value | int("hello") |
IndexError | List index out of range | lst[10] |
KeyError | Missing dict key | d["missing"] |
ZeroDivisionError | Division by zero | 10/0 |
FileNotFoundError | File doesn't exist | open("nofile.txt") |
ImportError | Module not found | import nonexistent |
AttributeError | Object has no such attribute | None.upper() |
Exception is broad; prefer specific exceptions. Use except (TypeError, ValueError) as e: for multiple types.3 try / except – Catching & Handling
The core mechanism. You can catch one, many, or all exceptions. Always capture the exception object for logging.
# Basic try/except
try:
result = 10 / int(input("Enter a number: "))
except ZeroDivisionError:
print("Cannot divide by zero!")
except ValueError:
print("That's not a valid number!")
except Exception as e:
print(f"Unexpected error: {e}")
# Multiple exceptions in one line
try:
data = {"a":1}
print(data["b"])
except (KeyError, IndexError) as e:
print(f"Lookup failed: {e}")
try/except allows your application to continue running, maybe retry or show a friendly message.4 else & finally – Success Path & Cleanup
else runs only if no exception occurred. finally runs no matter what — perfect for releasing resources.
def read_config(filename):
file = None
try:
file = open(filename, 'r')
data = file.read()
except FileNotFoundError:
print(f"Config file {filename} not found, using defaults")
data = {}
else:
print("Config loaded successfully")
finally:
if file:
file.close()
print("Cleanup done")
return data
else for code that should only run on success (keeps the try block minimal). Use finally for closing files, releasing locks, or restoring state — it's guaranteed.5 raise & Custom Exceptions
You can raise exceptions yourself to signal errors, and define your own exception classes for better readability and control.
class InsufficientFundsError(Exception):
"""Custom exception for banking domain."""
def __init__(self, balance, amount):
super().__init__(f"Balance {balance} is insufficient for withdrawal of {amount}")
self.balance = balance
self.amount = amount
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
# Usage
try:
new_balance = withdraw(100, 200)
except InsufficientFundsError as e:
print(e) # Balance 100 is insufficient for withdrawal of 200
ValueError, InsufficientFundsError tells you exactly what went wrong.6 Assertions – Debugging Aid
assert condition, message checks invariants. If the condition is false, it raises AssertionError. Disable assertions in production with -O flag.
def apply_discount(price, discount):
assert 0 <= discount <= 1, "Discount must be between 0 and 1"
return price * (1 - discount)
# This will fail during development
# apply_discount(100, 1.2) # AssertionError
if + raise ValueError for user‑facing checks.7 Production‑Grade Exception Handling
In production, you need logging, retries, graceful degradation, and monitoring. Here are essential patterns.
import logging
import time
from functools import wraps
logging.basicConfig(level=logging.ERROR)
def retry(max_attempts=3, delay=1, backoff=2):
"""Decorator to retry a function on exception."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
attempt = 0
while attempt < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempt += 1
logging.error(f"Attempt {attempt} failed: {e}")
if attempt == max_attempts:
raise
time.sleep(delay * (backoff ** (attempt-1)))
return wrapper
return decorator
@retry(max_attempts=3, delay=1)
def unstable_network_call():
# simulate transient failure
raise ConnectionError("Network timeout")
with) for resource safety. Implement circuit breakers for external calls.Solved Practice Problems
Work through these solved examples to solidify your understanding. Each solution includes a step‑by‑step explanation.
🔰 Easy (3 problems)
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
print("Error: Cannot divide by zero.")
return None
# Test
print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # Error: Cannot divide by zero. → None
def get_element(lst, index):
try:
return lst[index]
except IndexError:
print(f"Index {index} is out of range. Returning None.")
return None
numbers = [10, 20, 30]
print(get_element(numbers, 1)) # 20
print(get_element(numbers, 5)) # Error message + None
def get_config(key, config_dict):
try:
return config_dict[key]
except KeyError:
print(f"Key '{key}' not found. Using default.")
return "default_value"
config = {"host": "localhost", "port": 8080}
print(get_config("host", config)) # localhost
print(get_config("timeout", config)) # Key 'timeout' not found. Using default. → default_value
⚡ Intermediate (3 problems)
import json
def parse_and_access(json_string, key):
try:
data = json.loads(json_string)
return data[key]
except json.JSONDecodeError:
print("Invalid JSON format.")
except KeyError:
print(f"Key '{key}' missing in JSON.")
return None
# Example
good_json = '{"name": "Alice", "age": 30}'
bad_json = 'invalid'
print(parse_and_access(good_json, "name")) # Alice
print(parse_and_access(good_json, "city")) # Key missing
print(parse_and_access(bad_json, "name")) # Invalid JSON
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
super().__init__(f"Balance {balance} is insufficient for withdrawal of {amount}")
self.balance = balance
self.amount = amount
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
try:
new_balance = withdraw(100, 200)
except InsufficientFundsError as e:
print(e) # Balance 100 is insufficient for withdrawal of 200
import time
from functools import wraps
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f"Attempt {attempt+1} failed: {e}. Retrying...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=2, delay=0.5)
def unstable_call():
# simulate failure
raise ConnectionError("Network issue")
try:
unstable_call()
except ConnectionError:
print("All retries exhausted")
🚀 Advanced (3 problems)
class Transaction:
def __init__(self):
self.committed = False
def __enter__(self):
print("BEGIN TRANSACTION")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.committed = True
print("COMMIT")
else:
print(f"ROLLBACK due to {exc_type.__name__}: {exc_val}")
return False # don't suppress exception
def process_order():
with Transaction() as t:
# raise ValueError("Invalid order") # uncomment to test rollback
print("Order processed")
process_order()
class DataPipelineError(Exception):
pass
def parse_int(value):
try:
return int(value)
except ValueError as original:
raise DataPipelineError("Failed to convert to integer") from original
try:
parse_int("abc")
except DataPipelineError as e:
print(f"Pipeline error: {e}")
if e.__cause__:
print(f"Caused by: {e.__cause__}")
import sys
import traceback
from datetime import datetime
def global_exception_handler(exc_type, exc_value, exc_tb):
"""Log unhandled exceptions to a file."""
error_msg = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
with open('crash.log', 'a') as f:
f.write(f"[{datetime.now()}] UNHANDLED EXCEPTION\n{error_msg}\n")
print("A critical error occurred. Check crash.log", file=sys.stderr)
sys.excepthook = global_exception_handler
# Test – uncomment to see it in action (will crash but log)
# raise RuntimeError("Something went very wrong")
Unsolved Practice Problems
Test your understanding with these carefully graded exercises. No peeking — try to solve them first!
🔰 Easy (5 problems)
1. Safe Division
Write a function that takes two numbers and returns their division. If the denominator is zero, catch ZeroDivisionError and return None instead of crashing.
def safe_divide(a, b):
# your code here
pass2. List Element Access
Given a list and an index, return the element. If the index is out of range, catch IndexError and print "Index out of bounds".
3. Dictionary Lookup with Default
Write a function get_config(key, config_dict) that returns the value for the key. Catch KeyError and return "default_value" instead.
4. Integer Conversion
Ask the user for input and convert it to an integer. Catch ValueError and print "Not a valid integer".
5. File Reader
Try to open a file and read its content. Catch FileNotFoundError and print a friendly message.
⚡ Intermediate (5 problems)
6. Multiple Exception Handling
Implement a function that parses a JSON string and accesses a specific key. Catch both json.JSONDecodeError and KeyError, printing different messages for each.
7. Finally for Resource Cleanup
Write a context manager class (or use a function with try/finally) that opens a file, processes it, and ensures the file is closed even if an exception occurs.
8. Custom Exception Hierarchy
Create a base exception AppError and two subclasses DatabaseError and ValidationError. Write a function that raises the appropriate error based on a condition.
9. Retry with Backoff
Write a decorator @retry_on_failure(max_retries=3) that retries a function if it raises any Exception, with a 1‑second delay between retries.
10. Assertion vs Exception
Write a function that calculates the square root of a number. Use assert to check non‑negative input, and catch it only if assertions are enabled. Then write a production‑ready version that raises ValueError instead.
🚀 Advanced (5 problems)
11. Chained Exceptions
In a data pipeline, catch a ValueError while converting a string to int. Raise a new DataPipelineError with the original exception as __cause__ (using raise ... from err).
12. Context Manager with Error Handling
Create a context manager Transaction that simulates a database transaction. If an exception occurs inside the with block, roll back; otherwise commit. Use __exit__ to decide.
13. Global Exception Hook
Override sys.excepthook to log all unhandled exceptions to a file with timestamp and traceback, then exit gracefully.
14. Circuit Breaker Pattern
Implement a simple circuit breaker class that, after 3 consecutive failures, stops calling a function for a timeout period.
15. Nested try/except with Resource Cleanup
Write a function that opens two files (source and destination) and copies content. If opening the destination fails, ensure the source file is closed in an outer finally block. Use nested try structures.