Custom Exceptions: Tailoring Error Signals
- Built-in exceptions are great, but often too generic for application-specific failures.
- A custom exception like
ServiceConnectionError immediately conveys context compared to a plain Exception.
- Defining a base exception class groups related errors; subclasses add specificity for targeted handling.
- Catching
except BaseError: handles all related issues, while except SpecificError: addresses one case precisely.
Simple Custom Exceptions (Inheritance)
- Create a new exception by subclassing
Exception or another exception class.
- Using
pass is enough when no extra logic or attributes are needed.
- Catch the base class (
AutomationError) to handle any related subclass errors in one block.
- Use subclasses (
FileProcessingError, APICallError) when context-specific handling is required.
class AutomationError(Exception):
"""Base for all automation script errors."""
pass
class FileProcessingError(AutomationError):
"""Error during file processing stage."""
pass
class APICallError(AutomationError):
"""Error during an external API call."""
pass
def process_file(filepath):
raise FileProcessingError(f"Failed to process file at path: {filepath}")
try:
process_file("nonexistent.csv")
except FileProcessingError as e:
print(f"File error: {e}")
except AutomationError:
print("Other automation error occurred.")
Adding Context with __init__
- Override
__init__ in your exception class to capture context (e.g., filename, invalid value).
- Store custom attributes on
self and build a clear message passed to super().__init__().
- Inherit from a built-in exception (
ValueError) when semantics align, allowing broad catches.
- Attribute access (
e.key_name) provides extra debugging info in handlers.
class ConfigValueError(ValueError):
"""Raised when a config value is invalid."""
def __init__(self, key_name, invalid_value, message="Invalid configuration value."):
self.key_name = key_name
self.invalid_value = invalid_value
full_message = f"{message} for key '{key_name}': received '{invalid_value}'"
super().__init__(full_message)
try:
raise ConfigValueError("timeout", -5, message="Timeout cannot be negative")
except ConfigValueError as e:
print(f"{e}")
print(f" -> key: {e.key_name}")
print(f" -> value: {e.invalid_value}")
Raising and Catching Enhanced Custom Exceptions
- Raise custom exceptions by instantiating them with relevant arguments:
raise MyError(arg1, arg2).
- In
except blocks, catch specific exceptions and access their attributes for tailored recovery or logging.
- Fallback
except BaseError: catches any related subclass if no more specific handler exists.
class DeploymentError(Exception):
"""Base class for deployment-related errors."""
pass
class InvalidEnvironmentError(DeploymentError):
"""Raised when environment is invalid."""
def __init__(self, env_name, allowed_envs):
self.env_name = env_name
self.allowed_envs = allowed_envs
super().__init__(f"Invalid environment '{env_name}'. Allowed values: {allowed_envs}")
class PackageMissingError(DeploymentError):
"""Raised when required packages are missing."""
def __init__(self, package_name, host):
self.package_name = package_name
self.host = host
super().__init__(f"Package '{package_name}' is missing on host {host}.")
def deploy_app(environment, package):
allowed_envs = ["staging", "production"]
if environment not in allowed_envs:
raise InvalidEnvironmentError(environment, allowed_envs)
if environment == "production" and package == "critical-lib":
raise PackageMissingError(package, f"server-{environment}")
print(f"Deployment to {environment} with package {package} succeeded.")
for env, pkg in [("dev", "tool"), ("production", "critical-lib"), ("staging", "tool")]:
try:
deploy_app(env, pkg)
except DeploymentError as e:
print(e)