python

solutions

solutions.py🐍
"""
Solutions for Module 08 - Error Handling Exercises

These are reference solutions. Try to solve the exercises yourself first!
"""

import logging
from typing import Optional, Union
from contextlib import contextmanager


# =============================================================================
# Basic Exception Handling
# =============================================================================

def safe_divide(a: float, b: float) -> Optional[float]:
    """Divide a by b, return None if division by zero."""
    try:
        return a / b
    except ZeroDivisionError:
        return None


def safe_int_convert(value: str) -> Optional[int]:
    """Convert string to int, return None if invalid."""
    try:
        return int(value)
    except (ValueError, TypeError):
        return None


def safe_list_access(lst: list, index: int, default=None):
    """Access list item, return default if index out of range."""
    try:
        return lst[index]
    except IndexError:
        return default


def safe_dict_access(d: dict, key: str, default=None):
    """Access dict item with default."""
    try:
        return d[key]
    except KeyError:
        return default


def safe_file_read(filepath: str) -> Optional[str]:
    """Read file content, return None if file not found."""
    try:
        with open(filepath, 'r') as f:
            return f.read()
    except FileNotFoundError:
        return None
    except PermissionError:
        return None


# =============================================================================
# Multiple Exception Types
# =============================================================================

def process_user_input(value: str) -> dict:
    """
    Process user input with comprehensive error handling.
    
    Returns dict with 'success' and 'result' or 'error' keys.
    """
    try:
        # Try to parse as number
        if '.' in value:
            num = float(value)
        else:
            num = int(value)
        
        if num < 0:
            raise ValueError("Number must be positive")
        
        return {"success": True, "result": num}
    
    except ValueError as e:
        return {"success": False, "error": f"Invalid value: {e}"}
    except Exception as e:
        return {"success": False, "error": f"Unexpected error: {e}"}


def parse_json_safely(json_string: str) -> dict:
    """Parse JSON with error handling."""
    import json
    
    try:
        return {"success": True, "data": json.loads(json_string)}
    except json.JSONDecodeError as e:
        return {"success": False, "error": f"Invalid JSON: {e}"}
    except TypeError as e:
        return {"success": False, "error": f"Invalid input type: {e}"}


# =============================================================================
# Custom Exceptions
# =============================================================================

class ValidationError(Exception):
    """Raised when validation fails."""
    
    def __init__(self, field: str, message: str):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")


class InsufficientFundsError(Exception):
    """Raised when account has insufficient funds."""
    
    def __init__(self, required: float, available: float):
        self.required = required
        self.available = available
        self.deficit = required - available
        super().__init__(
            f"Insufficient funds: need ${required:.2f}, "
            f"have ${available:.2f} (deficit: ${self.deficit:.2f})"
        )


class RateLimitError(Exception):
    """Raised when rate limit is exceeded."""
    
    def __init__(self, retry_after: int):
        self.retry_after = retry_after
        super().__init__(f"Rate limit exceeded. Retry after {retry_after} seconds")


class ConfigurationError(Exception):
    """Raised when configuration is invalid."""
    
    def __init__(self, key: str, reason: str):
        self.key = key
        self.reason = reason
        super().__init__(f"Invalid configuration for '{key}': {reason}")


# =============================================================================
# Using Custom Exceptions
# =============================================================================

def validate_user_data(data: dict) -> dict:
    """Validate user data with custom exceptions."""
    if not data.get('username'):
        raise ValidationError('username', 'Username is required')
    
    if len(data.get('username', '')) < 3:
        raise ValidationError('username', 'Username must be at least 3 characters')
    
    if not data.get('email') or '@' not in data.get('email', ''):
        raise ValidationError('email', 'Valid email is required')
    
    age = data.get('age')
    if age is not None:
        if not isinstance(age, int) or age < 0 or age > 150:
            raise ValidationError('age', 'Age must be a valid number between 0 and 150')
    
    return {
        'username': data['username'],
        'email': data['email'],
        'age': age
    }


class BankAccount:
    """Bank account with custom exception handling."""
    
    def __init__(self, balance: float = 0.0):
        self._balance = balance
    
    @property
    def balance(self) -> float:
        return self._balance
    
    def withdraw(self, amount: float) -> float:
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive")
        if amount > self._balance:
            raise InsufficientFundsError(amount, self._balance)
        self._balance -= amount
        return self._balance
    
    def deposit(self, amount: float) -> float:
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self._balance += amount
        return self._balance


# =============================================================================
# Try-Finally and Cleanup
# =============================================================================

def process_with_cleanup(filepath: str):
    """Demonstrate try-finally for cleanup."""
    file = None
    try:
        file = open(filepath, 'r')
        # Process file
        data = file.read()
        return data
    except FileNotFoundError:
        return None
    finally:
        if file:
            file.close()


@contextmanager
def managed_resource(resource_name: str):
    """Context manager for resource management."""
    print(f"Acquiring {resource_name}")
    resource = {"name": resource_name, "active": True}
    try:
        yield resource
    finally:
        resource["active"] = False
        print(f"Released {resource_name}")


# =============================================================================
# Exception Chaining
# =============================================================================

class DataProcessingError(Exception):
    """Error during data processing."""
    pass


def fetch_data(source: str) -> dict:
    """Simulate data fetching."""
    if source == "invalid":
        raise ConnectionError("Could not connect to source")
    return {"value": 42}


def process_data(data: dict) -> int:
    """Simulate data processing."""
    if "value" not in data:
        raise KeyError("Missing 'value' key")
    return data["value"] * 2


def fetch_and_process(source: str) -> int:
    """Demonstrate exception chaining."""
    try:
        data = fetch_data(source)
    except ConnectionError as e:
        raise DataProcessingError("Failed to fetch data") from e
    
    try:
        return process_data(data)
    except KeyError as e:
        raise DataProcessingError("Failed to process data") from e


# =============================================================================
# Logging with Exceptions
# =============================================================================

# Configure logging
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)


def logged_operation(value: str) -> int:
    """Operation with logging."""
    logger.info(f"Processing value: {value}")
    
    try:
        result = int(value)
        logger.debug(f"Converted to integer: {result}")
        return result
    except ValueError as e:
        logger.error(f"Failed to convert '{value}': {e}")
        raise
    except Exception as e:
        logger.exception(f"Unexpected error processing '{value}'")
        raise


# =============================================================================
# Assertions
# =============================================================================

def calculate_average(numbers: list[float]) -> float:
    """Calculate average with assertions."""
    assert numbers, "List cannot be empty"
    assert all(isinstance(n, (int, float)) for n in numbers), "All items must be numbers"
    
    return sum(numbers) / len(numbers)


def validate_config(config: dict) -> None:
    """Validate configuration with assertions."""
    assert 'host' in config, "Missing 'host' in configuration"
    assert 'port' in config, "Missing 'port' in configuration"
    assert isinstance(config['port'], int), "Port must be an integer"
    assert 0 < config['port'] < 65536, "Port must be between 1 and 65535"


# =============================================================================
# Error Recovery Patterns
# =============================================================================

def with_retry(func, max_attempts: int = 3, delay: float = 1.0):
    """Retry a function on failure."""
    import time
    
    last_error = None
    for attempt in range(max_attempts):
        try:
            return func()
        except Exception as e:
            last_error = e
            if attempt < max_attempts - 1:
                time.sleep(delay)
    
    raise last_error


def with_fallback(primary, *fallbacks):
    """Try primary function, fall back to alternatives."""
    for func in (primary, *fallbacks):
        try:
            return func()
        except Exception:
            continue
    raise RuntimeError("All functions failed")


def graceful_degradation(operations: list, continue_on_error: bool = True) -> list:
    """
    Execute operations with graceful degradation.
    
    Returns list of results (or None for failed operations).
    """
    results = []
    for op in operations:
        try:
            result = op()
            results.append(result)
        except Exception as e:
            if continue_on_error:
                logger.warning(f"Operation failed: {e}")
                results.append(None)
            else:
                raise
    return results


# =============================================================================
# Test Solutions
# =============================================================================

if __name__ == "__main__":
    # Test basic exception handling
    assert safe_divide(10, 2) == 5.0
    assert safe_divide(10, 0) is None
    
    assert safe_int_convert("42") == 42
    assert safe_int_convert("not a number") is None
    
    assert safe_list_access([1, 2, 3], 1) == 2
    assert safe_list_access([1, 2, 3], 10, "default") == "default"
    
    # Test custom exceptions
    try:
        validate_user_data({})
    except ValidationError as e:
        assert e.field == 'username'
    
    account = BankAccount(100)
    account.withdraw(50)
    assert account.balance == 50
    
    try:
        account.withdraw(100)
    except InsufficientFundsError as e:
        assert e.deficit == 50
    
    # Test context manager
    with managed_resource("database") as res:
        assert res["active"]
    
    # Test assertions
    assert calculate_average([1, 2, 3, 4, 5]) == 3.0
    
    try:
        calculate_average([])
    except AssertionError:
        pass  # Expected
    
    print("All error handling solutions verified! ✓")
Solutions - Python Tutorial | DeepML