Table of Contents
Understanding Python Error Handling
Error handling allows your programs to gracefully handle unexpected situations and continue running instead of crashing.
Types of Errors
# Syntax Errors (caught before program runs)
# print("Hello World" # Missing closing parenthesis
# Runtime Errors (exceptions that occur during execution)
# print(10 / 0) # ZeroDivisionError
# print(undefined_var) # NameError
# print([1, 2, 3][5]) # IndexError
# Logic Errors (program runs but produces wrong results)
def calculate_average(numbers):
return sum(numbers) / len(numbers) + 1 # Logic error: +1 shouldn't be there
Basic Exception Handling
# Basic try-except block
try:
number = int(input("Enter a number: "))
result = 10 / number
print(f"Result: {result}")
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
except ValueError:
print("Error: Please enter a valid number!")
print("Program continues...")
# Handling multiple exceptions
def safe_divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
print("Cannot divide by zero!")
return None
except TypeError:
print("Both arguments must be numbers!")
return None
print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # Cannot divide by zero! None
print(safe_divide(10, "2")) # Both arguments must be numbers! None
Exception Handling with Else and Finally
def read_file_safely(filename):
try:
file = open(filename, 'r')
content = file.read()
except FileNotFoundError:
print(f"Error: File '{filename}' not found!")
return None
except PermissionError:
print(f"Error: Permission denied to read '{filename}'!")
return None
else:
# Executes only if no exception occurred
print(f"Successfully read {len(content)} characters")
return content
finally:
# Always executes, regardless of exceptions
try:
file.close()
print("File closed")
except:
pass # File might not have been opened
# Test the function
content = read_file_safely("test.txt")
if content:
print("File content:", content[:50] + "..." if len(content) > 50 else content)
Catching Multiple Exceptions
def process_data(data):
try:
# Convert to number
number = float(data)
# Perform calculation
result = 100 / number
# Access list element
values = [1, 2, 3]
selected = values[int(number)]
return result, selected
except (ValueError, TypeError) as e:
print(f"Data conversion error: {e}")
return None, None
except ZeroDivisionError:
print("Cannot divide by zero!")
return None, None
except IndexError as e:
print(f"Index out of range: {e}")
return None, None
except Exception as e:
# Catch any other exception
print(f"Unexpected error: {e}")
return None, None
# Test with different inputs
test_inputs = ["2", "0", "5", "abc", "-1"]
for data in test_inputs:
print(f"
Testing with: {data}")
result = process_data(data)
print(f"Result: {result}")
Raising Custom Exceptions
# Define custom exception classes
class InvalidAgeError(Exception):
"""Raised when age is invalid"""
def __init__(self, age, message="Age must be between 0 and 150"):
self.age = age
self.message = message
super().__init__(self.message)
class InsufficientFundsError(Exception):
"""Raised when account has insufficient funds"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
message = f"Insufficient funds: Balance ${balance}, Attempted withdrawal ${amount}"
super().__init__(message)
# Using custom exceptions
class Person:
def __init__(self, name, age):
self.name = name
self.set_age(age)
def set_age(self, age):
if not isinstance(age, (int, float)):
raise TypeError("Age must be a number")
if age < 0 or age > 150:
raise InvalidAgeError(age)
self.age = age
def __str__(self):
return f"{self.name}, age {self.age}"
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
return self.balance
# Test custom exceptions
try:
person = Person("Alice", 25)
print(person)
person.set_age(200) # This will raise InvalidAgeError
except InvalidAgeError as e:
print(f"Age error: {e}")
try:
account = BankAccount(100)
account.withdraw(150) # This will raise InsufficientFundsError
except InsufficientFundsError as e:
print(f"Banking error: {e}")
Exception Information and Traceback
import traceback
import sys
def problematic_function():
"""Function that will cause an error"""
data = [1, 2, 3]
return data[10] # IndexError
def get_exception_info():
try:
problematic_function()
except Exception as e:
# Get exception information
exc_type, exc_value, exc_traceback = sys.exc_info()
print("Exception Type:", exc_type.__name__)
print("Exception Value:", exc_value)
print("Exception Args:", e.args)
print("
Traceback:")
traceback.print_exc()
print("
Formatted traceback:")
tb_lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
for line in tb_lines:
print(line.strip())
get_exception_info()
# Logging exceptions
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('error.log'),
logging.StreamHandler()
]
)
def divide_with_logging(a, b):
try:
result = a / b
logging.info(f"Successfully divided {a} by {b} = {result}")
return result
except ZeroDivisionError as e:
logging.error(f"Division by zero: {a} / {b}", exc_info=True)
return None
except Exception as e:
logging.error(f"Unexpected error in division: {e}", exc_info=True)
return None
# Test logging
divide_with_logging(10, 2)
divide_with_logging(10, 0)
Context Managers and Exception Handling
# Custom context manager for database connections
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.connection = None
def __enter__(self):
print(f"Connecting to database: {self.db_name}")
# Simulate database connection
self.connection = f"Connection to {self.db_name}"
return self.connection
def __exit__(self, exc_type, exc_value, traceback):
print(f"Closing database connection: {self.db_name}")
if exc_type:
print(f"Exception occurred: {exc_type.__name__}: {exc_value}")
# Return False to propagate the exception
return False
return True
# Using the context manager
try:
with DatabaseConnection("mydb") as conn:
print(f"Using connection: {conn}")
# Simulate some database operations
if True: # Change to False to test exception handling
print("Database operations completed successfully")
else:
raise ValueError("Database operation failed")
except Exception as e:
print(f"Caught exception: {e}")
# Context manager with exception suppression
class ErrorSuppressor:
def __init__(self, *exception_types):
self.exception_types = exception_types
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type and issubclass(exc_type, self.exception_types):
print(f"Suppressed {exc_type.__name__}: {exc_value}")
return True # Suppress the exception
return False
# Using error suppressor
with ErrorSuppressor(ValueError, TypeError):
print("This will run")
raise ValueError("This error will be suppressed")
print("This won't run")
print("Program continues after suppressed error")
Debugging Techniques
# Using assert for debugging
def calculate_factorial(n):
assert isinstance(n, int), "Input must be an integer"
assert n >= 0, "Input must be non-negative"
if n == 0 or n == 1:
return 1
result = 1
for i in range(2, n + 1):
result *= i
# Debug assertion
assert result > 0, f"Factorial calculation error at i={i}"
return result
# Test assertions
try:
print(calculate_factorial(5)) # 120
print(calculate_factorial(-1)) # AssertionError
except AssertionError as e:
print(f"Assertion failed: {e}")
# Debug printing with decorators
def debug_calls(func):
"""Decorator to debug function calls"""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
try:
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
except Exception as e:
print(f"{func.__name__} raised {type(e).__name__}: {e}")
raise
return wrapper
@debug_calls
def divide_numbers(a, b):
return a / b
# Test debug decorator
divide_numbers(10, 2)
try:
divide_numbers(10, 0)
except ZeroDivisionError:
pass
Practice Exercise
Create a robust file processing system with comprehensive error handling:
import json
import csv
import os
import logging
from datetime import datetime
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('file_processor.log'),
logging.StreamHandler()
]
)
class FileProcessingError(Exception):
"""Custom exception for file processing errors"""
pass
class UnsupportedFileTypeError(FileProcessingError):
"""Raised when file type is not supported"""
pass
class DataValidationError(FileProcessingError):
"""Raised when data validation fails"""
pass
class FileProcessor:
"""Robust file processor with comprehensive error handling"""
SUPPORTED_EXTENSIONS = {'.txt', '.json', '.csv'}
def __init__(self):
self.processed_files = []
self.errors = []
def validate_file(self, filepath):
"""Validate file before processing"""
if not isinstance(filepath, str):
raise TypeError("Filepath must be a string")
if not os.path.exists(filepath):
raise FileNotFoundError(f"File not found: {filepath}")
if not os.path.isfile(filepath):
raise ValueError(f"Path is not a file: {filepath}")
_, ext = os.path.splitext(filepath.lower())
if ext not in self.SUPPORTED_EXTENSIONS:
raise UnsupportedFileTypeError(f"Unsupported file type: {ext}")
# Check file size (max 10MB)
file_size = os.path.getsize(filepath)
if file_size > 10 * 1024 * 1024:
raise FileProcessingError(f"File too large: {file_size} bytes")
return True
def process_text_file(self, filepath):
"""Process text file"""
try:
with open(filepath, 'r', encoding='utf-8') as file:
content = file.read()
# Basic validation
if not content.strip():
raise DataValidationError("Text file is empty")
# Process content
lines = content.split('
')
word_count = len(content.split())
char_count = len(content)
return {
'type': 'text',
'lines': len(lines),
'words': word_count,
'characters': char_count,
'content_preview': content[:100] + '...' if len(content) > 100 else content
}
except UnicodeDecodeError as e:
raise FileProcessingError(f"Text encoding error: {e}")
def process_json_file(self, filepath):
"""Process JSON file"""
try:
with open(filepath, 'r', encoding='utf-8') as file:
data = json.load(file)
# Validate JSON structure
if not isinstance(data, (dict, list)):
raise DataValidationError("JSON must contain object or array")
return {
'type': 'json',
'structure': type(data).__name__,
'size': len(data) if isinstance(data, (list, dict)) else 1,
'keys': list(data.keys()) if isinstance(data, dict) else None,
'sample': str(data)[:200] + '...' if len(str(data)) > 200 else str(data)
}
except json.JSONDecodeError as e:
raise FileProcessingError(f"Invalid JSON format: {e}")
def process_csv_file(self, filepath):
"""Process CSV file"""
try:
rows = []
with open(filepath, 'r', encoding='utf-8') as file:
# Try to detect delimiter
sample = file.read(1024)
file.seek(0)
sniffer = csv.Sniffer()
delimiter = sniffer.sniff(sample).delimiter
reader = csv.reader(file, delimiter=delimiter)
rows = list(reader)
if not rows:
raise DataValidationError("CSV file is empty")
# Validate structure
if len(rows) < 2:
raise DataValidationError("CSV must have at least header and one data row")
header = rows[0]
data_rows = rows[1:]
# Check for consistent column count
expected_cols = len(header)
for i, row in enumerate(data_rows, 2):
if len(row) != expected_cols:
logging.warning(f"Row {i} has {len(row)} columns, expected {expected_cols}")
return {
'type': 'csv',
'rows': len(data_rows),
'columns': len(header),
'headers': header,
'delimiter': delimiter,
'sample_row': data_rows[0] if data_rows else None
}
except csv.Error as e:
raise FileProcessingError(f"CSV processing error: {e}")
def process_file(self, filepath):
"""Main file processing method"""
start_time = datetime.now()
try:
# Validate file
self.validate_file(filepath)
logging.info(f"Processing file: {filepath}")
# Determine file type and process
_, ext = os.path.splitext(filepath.lower())
if ext == '.txt':
result = self.process_text_file(filepath)
elif ext == '.json':
result = self.process_json_file(filepath)
elif ext == '.csv':
result = self.process_csv_file(filepath)
else:
raise UnsupportedFileTypeError(f"Handler not implemented for {ext}")
# Add metadata
result.update({
'filepath': filepath,
'filename': os.path.basename(filepath),
'file_size': os.path.getsize(filepath),
'processed_at': datetime.now().isoformat(),
'processing_time': (datetime.now() - start_time).total_seconds()
})
self.processed_files.append(result)
logging.info(f"Successfully processed: {filepath}")
return result
except Exception as e:
error_info = {
'filepath': filepath,
'error_type': type(e).__name__,
'error_message': str(e),
'timestamp': datetime.now().isoformat()
}
self.errors.append(error_info)
logging.error(f"Failed to process {filepath}: {e}")
raise
def process_directory(self, directory_path):
"""Process all supported files in a directory"""
if not os.path.isdir(directory_path):
raise ValueError(f"Not a directory: {directory_path}")
results = []
errors = []
for filename in os.listdir(directory_path):
filepath = os.path.join(directory_path, filename)
if os.path.isfile(filepath):
try:
result = self.process_file(filepath)
results.append(result)
except Exception as e:
errors.append({
'file': filepath,
'error': str(e)
})
return {
'processed': len(results),
'errors': len(errors),
'results': results,
'error_details': errors
}
def generate_report(self):
"""Generate processing report"""
total_processed = len(self.processed_files)
total_errors = len(self.errors)
report = {
'summary': {
'total_files_processed': total_processed,
'total_errors': total_errors,
'success_rate': f"{(total_processed / (total_processed + total_errors) * 100):.1f}%" if (total_processed + total_errors) > 0 else "0%"
},
'file_types': {},
'errors_by_type': {},
'processed_files': self.processed_files,
'errors': self.errors
}
# Count file types
for file_info in self.processed_files:
file_type = file_info['type']
report['file_types'][file_type] = report['file_types'].get(file_type, 0) + 1
# Count error types
for error_info in self.errors:
error_type = error_info['error_type']
report['errors_by_type'][error_type] = report['errors_by_type'].get(error_type, 0) + 1
return report
# Example usage and testing
def demo_file_processor():
processor = FileProcessor()
# Create sample files for testing
sample_files = {
'sample.txt': "This is a sample text file.
It has multiple lines.
For testing purposes.",
'sample.json': '{"name": "John", "age": 30, "city": "New York"}',
'sample.csv': "Name,Age,City
Alice,25,Boston
Bob,30,Chicago"
}
# Create sample files
for filename, content in sample_files.items():
with open(filename, 'w') as f:
f.write(content)
# Process individual files
for filename in sample_files.keys():
try:
result = processor.process_file(filename)
print(f"
✅ Successfully processed {filename}:")
print(f" Type: {result['type']}")
print(f" Size: {result['file_size']} bytes")
print(f" Processing time: {result['processing_time']:.3f}s")
except Exception as e:
print(f"
❌ Failed to process {filename}: {e}")
# Test error cases
error_test_files = [
'nonexistent.txt', # File not found
'sample.xyz', # Unsupported type
]
for filename in error_test_files:
try:
processor.process_file(filename)
except Exception as e:
print(f"
⚠️ Expected error for {filename}: {e}")
# Generate and display report
report = processor.generate_report()
print(f"
📊 Processing Report:")
print(f" Files processed: {report['summary']['total_files_processed']}")
print(f" Errors: {report['summary']['total_errors']}")
print(f" Success rate: {report['summary']['success_rate']}")
# Clean up sample files
for filename in sample_files.keys():
try:
os.remove(filename)
except:
pass
# Run the demo
if __name__ == "__main__":
demo_file_processor()