Python Topic

Python Exception Handling

1. What are Exceptions?

Exceptions are events that occur during program execution that disrupt the normal flow of instructions.

Common Built-in Exceptions:

Exception Description Common Cause
ZeroDivisionError Division by zero 10 / 0
FileNotFoundError File doesn't exist open("nonexistent.txt")
ValueError Invalid value conversion int("abc")
TypeError Operation on wrong type "hello" + 5
IndexError Index out of range list[10] on a 3-item list
KeyError Dictionary key not found dict["missing_key"]
NameError Variable not defined print(undefined_var)

2. Basic Exception Handling: try and except

The Basic Structure:

python

try:
    # Code that might cause an exception
    risky_operation()
except ExceptionType:
    # Code to handle the exception
    handle_error()

Simple Examples:

python

# Handling division by zero
try:
    result = 10 / 0
    print(f"Result: {result}")
except ZeroDivisionError:
    print("Error: Cannot divide by zero!")

# Handling file operations
try:
    with open("nonexistent_file.txt", "r") as file:
        content = file.read()
except FileNotFoundError:
    print("Error: File not found!")

# Handling multiple specific exceptions
try:
    number = int(input("Enter a number: "))
    result = 100 / number
    print(f"100 / {number} = {result}")
except ValueError:
    print("Error: Please enter a valid number!")
except ZeroDivisionError:
    print("Error: Cannot divide by zero!")

3. Advanced Exception Handling

Catching Multiple Exceptions:

python

# Multiple exceptions in one except block
try:
    num = int(input("Enter a number: "))
    data = [1, 2, 3]
    print(data[num])
except (ValueError, IndexError) as e:
    print(f"Error: {e}")
    print("Please enter a valid index (0-2)")

# Separate except blocks for different handling
try:
    file = open("data.txt", "r")
    number = int(file.read())
    result = 100 / number
except FileNotFoundError:
    print("File not found. Using default value.")
    result = 0
except ValueError:
    print("Invalid data in file.")
    result = 0
except ZeroDivisionError:
    print("Cannot divide by zero.")
    result = 0

Getting Exception Information:

python

try:
    number = int("abc")
except ValueError as e:
    print(f"Error type: {type(e).__name__}")
    print(f"Error message: {e}")
    print(f"Full exception info: {repr(e)}")

The else Clause:

python

# else block runs only if no exception occurred
try:
    number = int(input("Enter a number: "))
    result = 100 / number
except ValueError:
    print("Invalid input! Please enter a number.")
except ZeroDivisionError:
    print("Cannot divide by zero!")
else:
    print(f"Operation successful! Result: {result}")
    # This only runs if no exceptions were raised

The finally Clause:

python

# finally block always runs, regardless of exceptions
try:
    file = open("data.txt", "r")
    content = file.read()
    number = int(content)
    result = 100 / number
except (FileNotFoundError, ValueError, ZeroDivisionError) as e:
    print(f"Error: {e}")
    result = None
finally:
    # This always executes - perfect for cleanup
    try:
        file.close()
        print("File closed successfully.")
    except:
        pass  # file might not have been opened
    print("Cleanup completed.")

4. Raising Exceptions

Purpose: You can deliberately raise exceptions when something goes wrong in your code.

Basic Raising:

python

def calculate_square_root(number):
    if number < 0:
        raise ValueError("Cannot calculate square root of negative number")
    return number ** 0.5

try:
    result = calculate_square_root(-4)
except ValueError as e:
    print(f"Error: {e}")  # Error: Cannot calculate square root of negative number

Re-raising Exceptions:

python

def process_data(data):
    try:
        # Some processing that might fail
        if not data:
            raise ValueError("Data cannot be empty")
        return data.upper()
    except Exception as e:
        print(f"Error in process_data: {e}")
        raise  # Re-raise the same exception

try:
    result = process_data("")
except ValueError as e:
    print(f"Caught error: {e}")

Creating Custom Exceptions:

python

# Custom exception class
class InsufficientFundsError(Exception):
    """Exception raised when account has insufficient funds."""
    
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        self.message = f"Insufficient funds: ${balance} available, but ${amount} requested"
        super().__init__(self.message)

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 amount

# Using custom exception
account = BankAccount(100)
try:
    account.withdraw(150)
except InsufficientFundsError as e:
    print(f"Withdrawal failed: {e}")
    print(f"Available: ${e.balance}, Requested: ${e.amount}")

5. Practical Examples

Example 1: Robust User Input Handling

python

def get_positive_number(prompt):
    """Get a positive number from user with validation."""
    while True:
        try:
            number = float(input(prompt))
            if number <= 0:
                raise ValueError("Number must be positive")
            return number
        except ValueError as e:
            print(f"Invalid input: {e}")
            print("Please try again.\n")

def get_age():
    """Get a valid age from user."""
    while True:
        try:
            age = int(input("Enter your age: "))
            if age < 0:
                raise ValueError("Age cannot be negative")
            if age > 150:
                raise ValueError("Please enter a realistic age")
            return age
        except ValueError as e:
            print(f"Error: {e}")
            print("Please enter a valid age (0-150)\n")

# Usage
try:
    age = get_age()
    weight = get_positive_number("Enter your weight (kg): ")
    height = get_positive_number("Enter your height (m): ")
    
    bmi = weight / (height ** 2)
    print(f"Your BMI: {bmi:.2f}")
    
except KeyboardInterrupt:
    print("\nProgram interrupted by user")
except Exception as e:
    print(f"Unexpected error: {e}")

Example 2: File Processing with Comprehensive Error Handling

python

import os
from datetime import datetime

def process_data_file(filename):
    """Process a data file with comprehensive error handling."""
    results = []
    
    try:
        # Check if file exists and is readable
        if not os.path.exists(filename):
            raise FileNotFoundError(f"File '{filename}' does not exist")
        
        if not os.access(filename, os.R_OK):
            raise PermissionError(f"No read permission for '{filename}'")
        
        # Open and process file
        with open(filename, 'r') as file:
            print(f"Processing file: {filename}")
            
            for line_num, line in enumerate(file, 1):
                try:
                    # Remove whitespace and skip empty lines
                    line = line.strip()
                    if not line or line.startswith('#'):
                        continue
                    
                    # Process data (example: convert to numbers and square them)
                    number = float(line)
                    squared = number ** 2
                    results.append(squared)
                    
                    print(f"Line {line_num}: {number} -> {squared}")
                    
                except ValueError as e:
                    print(f"Warning: Invalid data on line {line_num}: '{line}'")
                    continue  # Skip bad lines but continue processing
                    
    except FileNotFoundError as e:
        print(f"Error: {e}")
        return None
    except PermissionError as e:
        print(f"Error: {e}")
        return None
    except Exception as e:
        print(f"Unexpected error processing file: {e}")
        return None
    else:
        print(f"File processed successfully. {len(results)} valid entries found.")
        return results
    finally:
        print("File processing attempt completed.\n")

# Test the function
files_to_process = ["data.txt", "nonexistent.txt", "protected_file.txt"]

for file in files_to_process:
    results = process_data_file(file)
    if results:
        print(f"Results: {results}\n")

Example 3: Database Connection Simulator

python

import random
import time

class DatabaseConnectionError(Exception):
    """Custom exception for database connection issues."""
    pass

class DatabaseTimeoutError(Exception):
    """Custom exception for database timeouts."""
    pass

class DatabaseSimulator:
    def __init__(self):
        self.connected = False
    
    def connect(self):
        """Simulate database connection with random failures."""
        print("Attempting to connect to database...")
        time.sleep(1)  # Simulate connection delay
        
        # Randomly simulate different scenarios
        scenario = random.choice(["success", "connection_error", "timeout", "auth_error"])
        
        if scenario == "success":
            self.connected = True
            print("Database connected successfully!")
            return True
        elif scenario == "connection_error":
            raise DatabaseConnectionError("Cannot connect to database server")
        elif scenario == "timeout":
            raise DatabaseTimeoutError("Database connection timeout")
        elif scenario == "auth_error":
            raise PermissionError("Authentication failed")
    
    def execute_query(self, query):
        """Execute a database query."""
        if not self.connected:
            raise DatabaseConnectionError("Not connected to database")
        
        print(f"Executing query: {query}")
        time.sleep(0.5)
        
        # Simulate occasional query errors
        if random.random() < 0.3:  # 30% chance of error
            raise ValueError("Query execution error: Invalid syntax")
        
        return f"Results for: {query}"
    
    def disconnect(self):
        """Disconnect from database."""
        if self.connected:
            print("Disconnecting from database...")
            self.connected = False
        print("Database disconnected.")

def run_database_operations():
    """Run database operations with comprehensive error handling."""
    db = DatabaseSimulator()
    
    try:
        # Attempt to connect with retry logic
        max_retries = 3
        for attempt in range(max_retries):
            try:
                db.connect()
                break  # Connection successful, break out of retry loop
            except (DatabaseConnectionError, DatabaseTimeoutError) as e:
                print(f"Connection attempt {attempt + 1} failed: {e}")
                if attempt < max_retries - 1:
                    print("Retrying in 2 seconds...")
                    time.sleep(2)
                else:
                    print("Max retries reached. Giving up.")
                    return
            except PermissionError as e:
                print(f"Authentication failed: {e}")
                return  # Auth errors usually don't benefit from retries
        
        # Execute queries
        queries = ["SELECT * FROM users", "UPDATE settings SET value='new'", "INVALID QUERY"]
        
        for query in queries:
            try:
                result = db.execute_query(query)
                print(f"Query result: {result}")
            except ValueError as e:
                print(f"Query failed: {e}")
                # Continue with next query instead of stopping entirely
                continue
        
    except Exception as e:
        print(f"Unexpected error: {e}")
    
    finally:
        # Always disconnect, whether operations succeeded or failed
        db.disconnect()

# Run multiple simulations to see different scenarios
print("=== Database Operations Simulation ===\n")
for i in range(3):
    print(f"--- Simulation {i + 1} ---")
    run_database_operations()
    print()

Example 4: Configuration Manager with Validation

python

import json
import os

class ConfigValidationError(Exception):
    """Custom exception for configuration validation errors."""
    pass

class ConfigManager:
    def __init__(self, config_file="config.json"):
        self.config_file = config_file
        self.config = {}
    
    def load_config(self):
        """Load configuration from file with error handling."""
        try:
            if not os.path.exists(self.config_file):
                raise FileNotFoundError(f"Config file '{self.config_file}' not found")
            
            with open(self.config_file, 'r') as file:
                self.config = json.load(file)
            
            self._validate_config()
            print("✅ Configuration loaded successfully")
            
        except json.JSONDecodeError as e:
            raise ConfigValidationError(f"Invalid JSON in config file: {e}")
        except Exception as e:
            raise ConfigValidationError(f"Failed to load config: {e}")
    
    def _validate_config(self):
        """Validate configuration data."""
        required_fields = ['database', 'api_key', 'timeout']
        
        for field in required_fields:
            if field not in self.config:
                raise ConfigValidationError(f"Missing required field: '{field}'")
        
        # Validate specific fields
        if not isinstance(self.config['timeout'], (int, float)) or self.config['timeout'] <= 0:
            raise ConfigValidationError("Timeout must be a positive number")
        
        if not self.config['api_key'] or not isinstance(self.config['api_key'], str):
            raise ConfigValidationError("API key must be a non-empty string")
    
    def get_setting(self, key, default=None):
        """Get a configuration setting safely."""
        try:
            return self.config[key]
        except KeyError:
            if default is not None:
                return default
            raise ConfigValidationError(f"Setting '{key}' not found in config")

# Usage example
def initialize_application():
    """Initialize application with configuration."""
    config_manager = ConfigManager("app_config.json")
    
    try:
        config_manager.load_config()
        
        # Use configuration
        database = config_manager.get_setting('database')
        api_key = config_manager.get_setting('api_key')
        timeout = config_manager.get_setting('timeout')
        
        print(f"Database: {database}")
        print(f"API Key: {'*' * len(api_key)}")  # Mask API key
        print(f"Timeout: {timeout}s")
        
        return True
        
    except ConfigValidationError as e:
        print(f"❌ Configuration error: {e}")
        print("Please check your configuration file.")
        return False
    except Exception as e:
        print(f"❌ Unexpected error: {e}")
        return False

# Test with different scenarios
print("=== Configuration Manager Test ===")
initialize_application()

6. Best Practices

1. Be Specific with Exceptions:

python

# Good - specific exceptions
try:
    value = int(user_input)
except ValueError:
    print("Please enter a valid number")

# Avoid - too broad
try:
    value = int(user_input)
except:  # Catches ALL exceptions - dangerous!
    print("Something went wrong")

2. Don't Use Exceptions for Normal Flow:

python

# Bad - using exceptions for normal program flow
try:
    while True:
        item = next(iterator)
        process(item)
except StopIteration:
    pass  # This is the normal way loops end!

# Good - explicit check
for item in iterator:
    process(item)

3. Provide Helpful Error Messages:

python

try:
    config = load_config_file("settings.json")
except FileNotFoundError:
    print("Error: Configuration file 'settings.json' not found.")
    print("Please create the file or check the path.")
except json.JSONDecodeError as e:
    print(f"Error: Invalid JSON in configuration file: {e}")
    print("Please validate your JSON syntax.")

4. Use Finally for Cleanup:

python

def process_file(filename):
    file = None
    try:
        file = open(filename, 'r')
        # Process file
        return process_content(file.read())
    except IOError as e:
        print(f"File error: {e}")
        return None
    finally:
        if file:
            file.close()  # Always close the file

Short Takeaways:

  1. try: Code that might raise exceptions

  2. except: Handle specific exceptions

  3. else: Run if no exceptions occurred

  4. finally: Always run (perfect for cleanup)

  5. raise: Create your own exceptions

  6. Custom Exceptions: Make your error handling more descriptive

The goal of exception handling isn't to prevent all errors, but to handle them gracefully when they occur. Good error handling makes your programs more robust and user-friendly!

Exception handling is what separates amateur code from professional, production-ready applications.

 

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?