python
solutions
solutions.py🐍python
"""
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! ✓")