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:

  1. Python executes the code in the try block.
  2. If an exception occurs, the rest of the try block is skipped.
  3. If the exception type matches the one specified after except, the except block is executed.
  4. If no exception occurs, the except block 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()