Python Exceptions
Exceptions handle runtime errors in Python programs. Instead of crashing, programs can catch and handle errors gracefully. This tutorial covers exception types, handling, and creation.
What are Exceptions?
Exceptions are events that occur during program execution that disrupt normal flow. They signal errors or unusual conditions.
Common Exceptions
# ZeroDivisionError
# result = 10 / 0
# ValueError
# int("not_a_number")
# FileNotFoundError
# open("nonexistent_file.txt")
# IndexError
# my_list = [1, 2, 3]
# my_list[10]
# KeyError
# my_dict = {"a": 1}
# my_dict["missing_key"]
# TypeError
# "string" + 5These are built-in exceptions. See built-in exceptions documentation.
Try-Except Blocks
Use try-except to catch and handle exceptions.
Basic Exception Handling
try:
# Code that might raise an exception
result = 10 / 0
print(f"Result: {result}")
except ZeroDivisionError:
# Handle the specific exception
print("Cannot divide by zero!")
print("Program continues...")The try block contains risky code. The except block handles specific exceptions.
Catching Multiple Exceptions
def divide_numbers(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
return "Cannot divide by zero"
except TypeError:
return "Both arguments must be numbers"
except Exception as e:
return f"Unexpected error: {e}"
print(divide_numbers(10, 2)) # 5.0
print(divide_numbers(10, 0)) # Cannot divide by zero
print(divide_numbers(10, "a")) # Both arguments must be numbersCatch specific exceptions first, then general ones. Use Exception as a catch-all.
Exception Information
try:
numbers = [1, 2, 3]
print(numbers[5])
except IndexError as e:
print(f"IndexError occurred: {e}")
print(f"Exception type: {type(e)}")The as keyword captures the exception object for inspection.
Else and Finally
else runs if no exception occurs. finally always runs.
Using Else
def read_file(filename):
try:
with open(filename, "r") as file:
content = file.read()
except FileNotFoundError:
return f"File '{filename}' not found"
else:
# Only runs if no exception
return f"File read successfully. Content length: {len(content)}"
print(read_file("existing_file.txt"))
print(read_file("nonexistent.txt"))else is useful for code that should only run on success.
Using Finally
def process_file(filename):
file = None
try:
file = open(filename, "r")
content = file.read()
return content
except FileNotFoundError:
return "File not found"
finally:
# Always runs, even if exception occurs
if file:
file.close()
print("File closed")
result = process_file("example.txt")
print(f"Result: {result}")finally ensures cleanup code always runs. Useful for resource management.
Raising Exceptions
You can raise exceptions when something goes wrong.
Raising Built-in Exceptions
def validate_age(age):
if not isinstance(age, (int, float)):
raise TypeError("Age must be a number")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age seems unrealistic")
return age
try:
validate_age(-5)
except ValueError as e:
print(f"Validation error: {e}")
try:
validate_age("thirty")
except TypeError as e:
print(f"Type error: {e}")Use raise to throw exceptions. Choose appropriate exception types.
Custom Exceptions
Create your own exception classes for specific errors.
class InsufficientFundsError(Exception):
"""Raised when account has insufficient funds."""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"Insufficient funds. Balance: ${balance}, Required: ${amount}")
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
return self.balance
account = BankAccount(100)
try:
account.withdraw(150)
except InsufficientFundsError as e:
print(e)Inherit from Exception for custom exceptions. Provide meaningful messages.
Exception Hierarchy
Exceptions form a hierarchy. Catch parent classes to handle multiple types.
# ArithmeticError is parent of ZeroDivisionError, OverflowError, etc.
try:
# Some arithmetic operation
pass
except ArithmeticError:
print("Arithmetic error occurred")
# LookupError is parent of IndexError, KeyError
try:
my_dict = {}
my_dict["missing"]
except LookupError:
print("Lookup error occurred")
# Exception is parent of all built-in exceptions
try:
# Risky code
pass
except Exception as e:
print(f"An error occurred: {e}")Use parent classes for broad exception handling. See exception hierarchy.
Context Managers and Exceptions
Context managers handle resources and exceptions automatically.
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.connection = None
def __enter__(self):
self.connection = f"Connected to {self.db_name}"
print("Opening connection")
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
print("Closing connection")
if exc_type:
print(f"Exception occurred: {exc_val}")
return False # Don't suppress exceptions
with DatabaseConnection("mydb") as conn:
print(f"Using: {conn}")
# raise ValueError("Something went wrong")
print("Outside context")Context managers ensure cleanup even when exceptions occur. See context managers.
Best Practices
Specific Exception Handling
# Good: specific exceptions
try:
with open("file.txt", "r") as file:
data = file.read()
except FileNotFoundError:
print("File not found")
except PermissionError:
print("No permission to read file")
# Bad: too broad
try:
# code
pass
except Exception: # Catches everything, including KeyboardInterrupt
passCatch specific exceptions. Avoid bare except: clauses.
Exception Chaining
def process_data(data):
try:
# Process data
result = int(data)
return result
except ValueError as e:
# Chain the exception
raise ValueError(f"Invalid data format: {data}") from e
try:
process_data("not_a_number")
except ValueError as e:
print(f"Processing failed: {e}")
print(f"Original cause: {e.__cause__}")Use from to chain exceptions and preserve context.
Logging Exceptions
import logging
logging.basicConfig(level=logging.ERROR)
def risky_operation():
try:
return 10 / 0
except ZeroDivisionError as e:
logging.error(f"Division error: {e}")
raise # Re-raise after logging
try:
risky_operation()
except ZeroDivisionError:
print("Handled the error")Log exceptions for debugging while still handling them.
Resource Management
# Good: automatic cleanup
with open("file.txt", "r") as file:
content = file.read()
# Manual cleanup (error-prone)
file = open("file.txt", "r")
try:
content = file.read()
finally:
file.close()Prefer context managers over manual resource management.
Common Patterns
Retry Pattern
import time
def unreliable_operation(max_retries=3):
for attempt in range(max_retries):
try:
# Simulate unreliable operation
if attempt < 2:
raise ConnectionError("Network error")
return "Success!"
except ConnectionError as e:
if attempt == max_retries - 1:
raise # Re-raise on last attempt
print(f"Attempt {attempt + 1} failed: {e}")
time.sleep(1) # Wait before retry
return None
result = unreliable_operation()
print(result)Retry operations that might fail temporarily.
Validation with Exceptions
class ValidationError(Exception):
pass
def validate_user(user_data):
errors = []
if not user_data.get("name"):
errors.append("Name is required")
if not user_data.get("email"):
errors.append("Email is required")
elif "@" not in user_data["email"]:
errors.append("Invalid email format")
if errors:
raise ValidationError("; ".join(errors))
return True
try:
validate_user({"name": "Alice"})
except ValidationError as e:
print(f"Validation failed: {e}")Use custom exceptions for validation errors.
Summary
Exceptions allow graceful error handling:
- Use try-except for risky operations
- Catch specific exceptions first
- Use finally for cleanup
- Raise meaningful exceptions
- Create custom exceptions when needed
- Prefer context managers for resources
External Resources:
Related Tutorials: