Lecture 3: Exception Handling in Python
1. The try-except Block
Basic Syntax
try:
# Code that might raise an exception
result = 10 / 0 # This will raise a ZeroDivisionError
except ZeroDivisionError as e:
# Code to handle the exception
print(f"Error: {e}")
result = float('inf') # Default value
How it works:
- Python executes the code in the
tryblock. - If an exception occurs, the rest of the
tryblock is skipped. - If the exception type matches the one specified after
except, the except block is executed. - If no exception occurs, the
exceptblock is skipped.
2. Handling Multiple Exceptions
def divide_numbers(a, b):
try:
result = a / b
print(f"{a} / {b} = {result}")
except ZeroDivisionError:
print("Error: Division by zero is not allowed!")
except TypeError:
print("Error: Both arguments must be numbers!")
except Exception as e:
print(f"An unexpected error occurred: {e}")
# Test cases
divide_numbers(10, 2) # Valid division
divide_numbers(10, 0) # ZeroDivisionError
divide_numbers("10", 2) # TypeError
Handling Multiple Exceptions in a Single Block
try:
# Code that might raise exceptions
value = int("not_a_number")
result = 10 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Error: {e.__class__.__name__} - {e}")
3. The else and finally Clauses
The else Block
Code in the else block runs only if no exceptions were raised in the try block.
def process_file(filename):
try:
with open(filename, 'r') as file:
data = file.read()
except FileNotFoundError:
print(f"Error: {filename} not found!")
else:
# This runs only if no exception occurred
print(f"Successfully read {len(data)} characters from {filename}")
return data
The finally Block
Code in the finally block always runs, whether an exception occurred or not.
def divide_with_finally(a, b):
try:
result = a / b
print(f"Result: {result}")
return result # This return is delayed until finally completes
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
return None # This return is also delayed
finally:
# This always runs, even if there's a return statement
print("Division operation completed")
# Test the function
divide_with_finally(10, 2)
divide_with_finally(10, 0)
4. Raising Exceptions
You can raise exceptions using the raise statement.
Raising Built-in Exceptions
def validate_age(age):
if age < 0:
raise ValueError("Age cannot be negative!")
if age > 120:
raise ValueError("Age seems unrealistic!")
return True
# Test the function
try:
validate_age(25) # Valid
validate_age(-5) # Raises ValueError
except ValueError as e:
print(f"Validation error: {e}")
Re-raising Exceptions
def process_data(data):
try:
# Some processing that might fail
result = int(data)
return result * 2
except ValueError as e:
print("Error in process_data:", e)
# Re-raise the exception to be handled by the caller
raise
try:
process_data("123") # Valid
process_data("abc") # Will raise ValueError
except ValueError as e:
print(f"Caught in main: {e}")
5. The assert Statement
The assert statement is used for debugging and testing. It raises an AssertionError if the condition is False.
def calculate_average(scores):
# Precondition: scores is not empty
assert len(scores) > 0, "Scores list cannot be empty"
# Precondition: all scores are between 0 and 100
for score in scores:
assert 0 <= score <= 100, f"Invalid score: {score}. Must be between 0 and 100"
average = sum(scores) / len(scores)
# Postcondition: average is between 0 and 100
assert 0 <= average <= 100, "Average is out of expected range"
return average
# Test cases
print(calculate_average([85, 90, 78, 92])) # Valid
# print(calculate_average([])) # AssertionError: Scores list cannot be empty
# print(calculate_average([85, 110, 78])) # AssertionError: Invalid score: 110
Important Note:
Assertions can be disabled by running Python with the -O (optimize) flag. They should be used for debugging and testing, not for handling expected error conditions in production code.
6. Creating Custom Exceptions
You can create custom exceptions by subclassing the Exception class.
class InvalidEmailError(Exception):
"""Raised when an email address is invalid."""
pass
class InvalidAgeError(Exception):
"""Raised when age is invalid."""
def __init__(self, age, message="Age must be between 0 and 120"):
self.age = age
self.message = message
super().__init__(self.message)
def validate_user(email, age):
if "@" not in email or "." not in email:
raise InvalidEmailError(f"Invalid email format: {email}")
if not (0 <= age <= 120):
raise InvalidAgeError(age)
print(f"User {email} is valid")
# Test the function
try:
validate_user("user@example.com", 25) # Valid
validate_user("invalid-email", 25) # Raises InvalidEmailError
validate_user("user@example.com", 150) # Raises InvalidAgeError
except (InvalidEmailError, InvalidAgeError) as e:
print(f"Validation error: {e}")
7. Practical Example: File Processing with Error Handling
import json
from datetime import datetime
class DataProcessingError(Exception):
"""Base exception for data processing errors."""
pass
class InvalidDataError(DataProcessingError):
"""Raised when data format is invalid."""
pass
class DataProcessor:
def __init__(self, input_file, output_file):
self.input_file = input_file
self.output_file = output_file
self.processed_count = 0
self.error_count = 0
def process_data(self, data):
"""Process a single data record."""
try:
# Validate required fields
required_fields = ['id', 'name', 'value']
for field in required_fields:
if field not in data:
raise InvalidDataError(f"Missing required field: {field}")
# Validate data types
if not isinstance(data['id'], int) or data['id'] <= 0:
raise InvalidDataError("ID must be a positive integer")
if not isinstance(data['name'], str) or not data['name'].strip():
raise InvalidDataError("Name must be a non-empty string")
# Add processing timestamp
data['processed_at'] = datetime.now().isoformat()
self.processed_count += 1
return data
except Exception as e:
self.error_count += 1
if not isinstance(e, DataProcessingError):
e = DataProcessingError(f"Error processing record: {e}")
print(f"Error: {e}")
return None
def run(self):
"""Process all records in the input file."""
try:
# Read input file
with open(self.input_file, 'r') as f:
try:
data = json.load(f)
except json.JSONDecodeError as e:
raise InvalidDataError(f"Invalid JSON: {e}")
# Process each record
processed_data = []
for record in data:
result = self.process_data(record)
if result is not None:
processed_data.append(result)
# Write output file
with open(self.output_file, 'w') as f:
json.dump(processed_data, f, indent=2)
# Print summary
print(f"Processing complete!")
print(f"Successfully processed: {self.processed_count} records")
print(f"Errors encountered: {self.error_count}")
except FileNotFoundError as e:
print(f"Error: Input file not found: {self.input_file}")
except PermissionError as e:
print(f"Error: Permission denied when accessing file: {e.filename}")
except Exception as e:
print(f"Unexpected error: {e}")
raise # Re-raise unexpected errors
# Example usage
if __name__ == "__main__":
import sys
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} ")
sys.exit(1)
processor = DataProcessor(sys.argv[1], sys.argv[2])
processor.run()