Python Exception Handling — try, except, finally, and Custom Exceptions
Advertisement
Introduction
Why This Matters
Runtime errors are inevitable. Files go missing, networks fail, APIs return unexpected data, and users provide invalid input. Unhandled exceptions crash programs and expose confusing stack traces to users. Python's exception handling system gives you the tools to anticipate failures, recover gracefully, and provide meaningful error messages.
In production systems, proper exception handling is the difference between a self-healing application that logs errors and continues serving users, versus one that crashes and requires manual intervention. Libraries like FastAPI, SQLAlchemy, and Django all rely on exception handling for request validation, transaction rollback, and HTTP error responses.
Understanding Python's exception hierarchy and how to create custom exception classes is also a key differentiator in engineering interviews and senior-level code reviews.
Basic try/except
try:
number = int(input("Enter a number: "))
result = 100 / number
print(f"Result: {result}")
except ValueError:
print("Please enter a valid integer.")
except ZeroDivisionError:
print("Division by zero is not allowed.")Catching Multiple Exceptions
def parse_config(path: str) -> dict:
try:
with open(path) as f:
import json
return json.load(f)
except FileNotFoundError:
print(f"Config file not found: {path}")
return {}
except json.JSONDecodeError as e:
print(f"Invalid JSON in config: {e}")
return {}
except (PermissionError, OSError) as e:
print(f"Cannot read config file: {e}")
return {}else and finally
def read_file(path: str) -> str:
f = None
try:
f = open(path)
content = f.read()
except FileNotFoundError:
print("File not found")
return ""
else:
# Runs only if no exception was raised
print(f"Successfully read {len(content)} characters")
return content
finally:
# Always runs — cleanup code
if f:
f.close()
print("File closed")Context Managers and finally
The with statement handles cleanup automatically and is preferred over manual finally:
def read_file_safe(path: str) -> str:
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
return ""Accessing Exception Information
import traceback
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Error type: {type(e).__name__}") # ZeroDivisionError
print(f"Error message: {e}") # division by zero
traceback.print_exc() # full stack trace to stderrCustom Exception Classes
Custom exceptions make errors self-documenting and allow callers to catch specific failures.
class AppError(Exception):
"""Base exception for this application."""
pass
class ValidationError(AppError):
def __init__(self, field: str, message: str):
self.field = field
self.message = message
super().__init__(f"Validation failed for '{field}': {message}")
class NotFoundError(AppError):
def __init__(self, resource: str, resource_id):
self.resource = resource
self.resource_id = resource_id
super().__init__(f"{resource} with id={resource_id} not found")
def get_user(user_id: int) -> dict:
users = {1: {"name": "Alice"}}
if user_id not in users:
raise NotFoundError("User", user_id)
return users[user_id]
try:
user = get_user(99)
except NotFoundError as e:
print(e) # User with id=99 not found
except AppError as e:
print(f"Application error: {e}")Exception Chaining with raise from
class DatabaseError(Exception):
pass
def fetch_user(user_id: int):
try:
# Simulate DB failure
raise ConnectionError("DB connection refused")
except ConnectionError as e:
raise DatabaseError("Failed to fetch user") from e
try:
fetch_user(1)
except DatabaseError as e:
print(e)
print(f"Caused by: {e.__cause__}")Raising Exceptions
def set_age(age: int) -> None:
if not isinstance(age, int):
raise TypeError(f"Age must be int, got {type(age).__name__}")
if age < 0 or age > 150:
raise ValueError(f"Age must be between 0 and 150, got {age}")
# Re-raise an exception after logging
try:
set_age(-5)
except ValueError as e:
print(f"Logging error: {e}")
raise # re-raise original exceptionCommon Mistakes
- Using a bare
except:clause — catchesSystemExitandKeyboardInterrupttoo - Catching
Exceptionas a catch-all instead of specific exception types - Swallowing exceptions silently with
passinexceptblocks - Raising a new exception inside
exceptwithoutfrom— hides the original cause - Not cleaning up resources in
finally— always usewithstatements when available
Best Practices
- Catch the most specific exception type possible
- Always use
fromwhen chaining exceptions to preserve context - Define a custom exception hierarchy rooted at a base
AppErrorclass - Log exceptions with full tracebacks using the
loggingmodule, notprint() - Use context managers (
with) to guarantee resource cleanup
Key Takeaways
try/exceptcatches exceptions;elseruns if no exception;finallyalways runs- Catch specific exceptions (e.g.,
ValueError) rather than broadException - Custom exceptions inherit from
Exceptionand make error handling self-documenting - Use
raise ExceptionType from originalto chain exceptions and preserve context traceback.print_exc()orlogging.exception()logs the full stack trace- Avoid bare
except:— it catches everything including keyboard interrupts - The
withstatement is the cleanest way to ensure resources are released
Advertisement