Python
Intermediate
Python Error Handling: Exceptions and Debugging
60 views
45 min read
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()