Python Intermediate

Python Error Handling: Exceptions and Debugging

CodingerWeb
CodingerWeb
20 views 45 min read

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()