mirror of
https://github.com/blshaer/python-by-example.git
synced 2026-03-27 23:29:25 +01:00
409 lines
12 KiB
Python
409 lines
12 KiB
Python
"""
|
|
================================================================================
|
|
File: 02_custom_exceptions.py
|
|
Topic: Creating and Using Custom Exceptions
|
|
================================================================================
|
|
|
|
This file demonstrates how to create your own exception classes in Python.
|
|
Custom exceptions make your code more expressive and allow you to handle
|
|
specific error conditions in a meaningful way.
|
|
|
|
Key Concepts:
|
|
- Why use custom exceptions
|
|
- Creating exception classes
|
|
- Exception with custom attributes
|
|
- Exception chaining
|
|
- Best practices
|
|
|
|
================================================================================
|
|
"""
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 1. Why Custom Exceptions?
|
|
# -----------------------------------------------------------------------------
|
|
|
|
print("--- Why Custom Exceptions? ---")
|
|
|
|
# Built-in exceptions are generic
|
|
# Custom exceptions:
|
|
# - Are more descriptive
|
|
# - Can carry additional data
|
|
# - Allow specific handling
|
|
# - Document error conditions
|
|
|
|
print("Benefits of custom exceptions:")
|
|
print(" - More meaningful error messages")
|
|
print(" - Carry context-specific data")
|
|
print(" - Enable targeted exception handling")
|
|
print(" - Self-documenting code")
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 2. Basic Custom Exception
|
|
# -----------------------------------------------------------------------------
|
|
|
|
print("\n--- Basic Custom Exception ---")
|
|
|
|
# Inherit from Exception (or a more specific built-in)
|
|
class InvalidAgeError(Exception):
|
|
"""Raised when an age value is invalid."""
|
|
pass
|
|
|
|
# Using the custom exception
|
|
def set_age(age):
|
|
"""Set age with validation."""
|
|
if age < 0:
|
|
raise InvalidAgeError("Age cannot be negative")
|
|
if age > 150:
|
|
raise InvalidAgeError("Age seems unrealistically high")
|
|
return age
|
|
|
|
# Catching custom exception
|
|
try:
|
|
set_age(-5)
|
|
except InvalidAgeError as e:
|
|
print(f"Caught InvalidAgeError: {e}")
|
|
|
|
try:
|
|
set_age(200)
|
|
except InvalidAgeError as e:
|
|
print(f"Caught InvalidAgeError: {e}")
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 3. Custom Exception with Attributes
|
|
# -----------------------------------------------------------------------------
|
|
|
|
print("\n--- Exception with Custom Attributes ---")
|
|
|
|
class ValidationError(Exception):
|
|
"""Raised when validation fails."""
|
|
|
|
def __init__(self, field, value, message):
|
|
self.field = field
|
|
self.value = value
|
|
self.message = message
|
|
# Call parent constructor with full message
|
|
super().__init__(f"{field}: {message} (got: {value})")
|
|
|
|
def to_dict(self):
|
|
"""Convert error to dictionary (useful for API responses)."""
|
|
return {
|
|
"field": self.field,
|
|
"value": self.value,
|
|
"message": self.message
|
|
}
|
|
|
|
# Using the enhanced exception
|
|
def validate_email(email):
|
|
"""Validate email format."""
|
|
if not email:
|
|
raise ValidationError("email", email, "Email is required")
|
|
if "@" not in email:
|
|
raise ValidationError("email", email, "Invalid email format")
|
|
return True
|
|
|
|
try:
|
|
validate_email("invalid-email")
|
|
except ValidationError as e:
|
|
print(f"Error: {e}")
|
|
print(f"Field: {e.field}")
|
|
print(f"Value: {e.value}")
|
|
print(f"As dict: {e.to_dict()}")
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 4. Exception Hierarchy
|
|
# -----------------------------------------------------------------------------
|
|
|
|
print("\n--- Exception Hierarchy ---")
|
|
|
|
# Create a base exception for your module/application
|
|
class AppError(Exception):
|
|
"""Base exception for the application."""
|
|
pass
|
|
|
|
class DatabaseError(AppError):
|
|
"""Database-related errors."""
|
|
pass
|
|
|
|
class ConnectionError(DatabaseError):
|
|
"""Database connection errors."""
|
|
pass
|
|
|
|
class QueryError(DatabaseError):
|
|
"""Query execution errors."""
|
|
pass
|
|
|
|
class AuthenticationError(AppError):
|
|
"""Authentication-related errors."""
|
|
pass
|
|
|
|
class PermissionError(AppError):
|
|
"""Permission-related errors."""
|
|
pass
|
|
|
|
# Now you can catch broadly or specifically
|
|
def database_operation():
|
|
raise QueryError("Invalid SQL syntax")
|
|
|
|
try:
|
|
database_operation()
|
|
except DatabaseError as e:
|
|
# Catches ConnectionError and QueryError
|
|
print(f"Database error: {e}")
|
|
except AppError as e:
|
|
# Catches all app errors
|
|
print(f"App error: {e}")
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 5. Exception Chaining
|
|
# -----------------------------------------------------------------------------
|
|
|
|
print("\n--- Exception Chaining ---")
|
|
|
|
class ProcessingError(Exception):
|
|
"""Error during data processing."""
|
|
pass
|
|
|
|
def parse_config(data):
|
|
"""Parse configuration data."""
|
|
try:
|
|
# Simulating a parsing error
|
|
if not data:
|
|
raise ValueError("Empty data")
|
|
return {"parsed": data}
|
|
except ValueError as e:
|
|
# Chain the original exception
|
|
raise ProcessingError("Failed to parse config") from e
|
|
|
|
try:
|
|
parse_config("")
|
|
except ProcessingError as e:
|
|
print(f"Processing error: {e}")
|
|
print(f"Caused by: {e.__cause__}")
|
|
|
|
# Using 'from None' to suppress chaining
|
|
def simple_error():
|
|
try:
|
|
raise ValueError("original")
|
|
except ValueError:
|
|
raise RuntimeError("new error") from None # Hides original
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 6. Practical Example: User Registration
|
|
# -----------------------------------------------------------------------------
|
|
|
|
print("\n--- Practical Example: User Registration ---")
|
|
|
|
class RegistrationError(Exception):
|
|
"""Base class for registration errors."""
|
|
pass
|
|
|
|
class UsernameError(RegistrationError):
|
|
"""Username-related errors."""
|
|
def __init__(self, username, reason):
|
|
self.username = username
|
|
self.reason = reason
|
|
super().__init__(f"Username '{username}': {reason}")
|
|
|
|
class PasswordError(RegistrationError):
|
|
"""Password-related errors."""
|
|
def __init__(self, issues):
|
|
self.issues = issues
|
|
super().__init__(f"Password issues: {', '.join(issues)}")
|
|
|
|
class EmailError(RegistrationError):
|
|
"""Email-related errors."""
|
|
pass
|
|
|
|
def validate_username(username):
|
|
"""Validate username."""
|
|
if len(username) < 3:
|
|
raise UsernameError(username, "Too short (min 3 chars)")
|
|
if not username.isalnum():
|
|
raise UsernameError(username, "Must be alphanumeric")
|
|
# Check if username exists (simulated)
|
|
existing = ["admin", "root", "user"]
|
|
if username.lower() in existing:
|
|
raise UsernameError(username, "Already taken")
|
|
return True
|
|
|
|
def validate_password(password):
|
|
"""Validate password strength."""
|
|
issues = []
|
|
if len(password) < 8:
|
|
issues.append("too short")
|
|
if not any(c.isupper() for c in password):
|
|
issues.append("needs uppercase")
|
|
if not any(c.islower() for c in password):
|
|
issues.append("needs lowercase")
|
|
if not any(c.isdigit() for c in password):
|
|
issues.append("needs digit")
|
|
|
|
if issues:
|
|
raise PasswordError(issues)
|
|
return True
|
|
|
|
def register_user(username, password, email):
|
|
"""Register a new user."""
|
|
try:
|
|
validate_username(username)
|
|
validate_password(password)
|
|
# validate_email(email) - already defined above
|
|
print(f" ✓ User '{username}' registered successfully!")
|
|
return True
|
|
except RegistrationError as e:
|
|
print(f" ✗ Registration failed: {e}")
|
|
return False
|
|
|
|
# Test registration
|
|
print("Registration attempts:")
|
|
register_user("ab", "password123", "test@example.com")
|
|
register_user("admin", "Password1", "test@example.com")
|
|
register_user("newuser", "weak", "test@example.com")
|
|
register_user("newuser", "StrongPass123", "test@example.com")
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 7. Context Manager with Exceptions
|
|
# -----------------------------------------------------------------------------
|
|
|
|
print("\n--- Exception in Context Manager ---")
|
|
|
|
class ManagedResource:
|
|
"""Resource with cleanup that handles exceptions."""
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def __enter__(self):
|
|
print(f" Acquiring resource: {self.name}")
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
print(f" Releasing resource: {self.name}")
|
|
if exc_type is not None:
|
|
print(f" Exception occurred: {exc_type.__name__}: {exc_val}")
|
|
# Return True to suppress the exception
|
|
# Return False to propagate it
|
|
return False # Don't suppress exceptions
|
|
|
|
# Using the context manager
|
|
print("Context manager with exception:")
|
|
try:
|
|
with ManagedResource("database") as resource:
|
|
print(f" Using {resource.name}")
|
|
raise RuntimeError("Something went wrong!")
|
|
except RuntimeError:
|
|
print(" Caught exception outside")
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 8. Re-raising Exceptions
|
|
# -----------------------------------------------------------------------------
|
|
|
|
print("\n--- Re-raising Exceptions ---")
|
|
|
|
def process_data(data):
|
|
"""Process data with logging and re-raise."""
|
|
try:
|
|
# Processing that might fail
|
|
if not data:
|
|
raise ValueError("Empty data")
|
|
return data.upper()
|
|
except ValueError as e:
|
|
print(f" Logging error: {e}")
|
|
raise # Re-raise the same exception
|
|
|
|
try:
|
|
process_data("")
|
|
except ValueError as e:
|
|
print(f"Caught re-raised exception: {e}")
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 9. Exception Documentation
|
|
# -----------------------------------------------------------------------------
|
|
|
|
print("\n--- Exception Documentation ---")
|
|
|
|
class APIError(Exception):
|
|
"""
|
|
Base exception for API errors.
|
|
|
|
Attributes:
|
|
status_code: HTTP status code
|
|
message: Human-readable error message
|
|
error_code: Application-specific error code
|
|
|
|
Example:
|
|
>>> raise APIError(404, "Resource not found", "NOT_FOUND")
|
|
"""
|
|
|
|
def __init__(self, status_code: int, message: str, error_code: str = None):
|
|
"""
|
|
Initialize API error.
|
|
|
|
Args:
|
|
status_code: HTTP status code (e.g., 400, 404, 500)
|
|
message: Human-readable error description
|
|
error_code: Optional application error code
|
|
"""
|
|
self.status_code = status_code
|
|
self.message = message
|
|
self.error_code = error_code
|
|
super().__init__(message)
|
|
|
|
def to_response(self):
|
|
"""Convert to API response format."""
|
|
return {
|
|
"error": {
|
|
"code": self.error_code,
|
|
"message": self.message,
|
|
"status": self.status_code
|
|
}
|
|
}
|
|
|
|
# Using documented exception
|
|
try:
|
|
raise APIError(404, "User not found", "USER_NOT_FOUND")
|
|
except APIError as e:
|
|
print(f"API Error Response: {e.to_response()}")
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 10. Best Practices
|
|
# -----------------------------------------------------------------------------
|
|
|
|
print("\n--- Best Practices ---")
|
|
|
|
best_practices = """
|
|
1. Inherit from Exception, not BaseException
|
|
- BaseException includes SystemExit, KeyboardInterrupt
|
|
|
|
2. Create a hierarchy for related exceptions
|
|
- Base exception for your module/app
|
|
- Specific exceptions inherit from base
|
|
|
|
3. Add useful attributes
|
|
- Include context that helps debugging
|
|
- Provide methods for serialization (API responses)
|
|
|
|
4. Document your exceptions
|
|
- What conditions trigger them
|
|
- What attributes they have
|
|
- How to handle them
|
|
|
|
5. Use meaningful names
|
|
- End with 'Error' or 'Exception'
|
|
- Be specific: UserNotFoundError, not JustError
|
|
|
|
6. Don't over-catch
|
|
- except Exception: catches too much
|
|
- Be specific about what you expect
|
|
|
|
7. Re-raise when appropriate
|
|
- Log and re-raise for debugging
|
|
- Transform to appropriate exception type
|
|
|
|
8. Use exception chaining
|
|
- Use 'from' to preserve original exception
|
|
- Helps with debugging complex systems
|
|
"""
|
|
|
|
print(best_practices)
|