Robust Error Handling in Python: Assertions, Exceptions, and Custom Error Types
Python distinguishes between two fundamental failure categories: syntax errors, which prevent code execution entirely, and exceptions, which occur during program execution. Six keywords govern error handling: assert, raise, try, except, else, and finally.
Assertions for Defensive Programming
The assert statement evaluates conditional expressions, triggering AssertionError when the condition evaluates to False. This mechanism serves debugging and internal consistency checks.
assert condition [, message]
Functionally equivalent to:
if not condition:
raise AssertionError(message)
Consider a configuration validation scenario:
import sys
def validate_environment():
minimum_version = (3, 8)
current = sys.version_info[:2]
assert current >= minimum_version, f"Python {minimum_version}+ required"
print(f"Running on Python {current[0]}.{current[1]}")
assert 'production' in sys.argv or 'staging' in sys.argv, \
"Deployment target not specified"
if __name__ == '__main__':
validate_environment()
Execution on incompatible versions raises:
AssertionError: Python (3, 8)+ required
Exception Handling Constructs
Unlike syntax errors detected during parsing, exceptions represent runtime anomalies. Python's exception mechanism parallels Java or C++ implementations but includes the distinctive else clause.
Basic Exception Capture
def process_data(filename):
try:
with open(filename, 'r') as handle:
content = handle.read()
value = int(content.strip())
result = 1000 / value
except FileNotFoundError as err:
print(f"Source file missing: {err}")
except ValueError as err:
print(f"Invalid data format: {err}")
except ZeroDivisionError:
print("Calculation failed: zero divisor")
except Exception as err:
print(f"Unexpected failure: {err}")
finally:
print("Processing attempt completed")
if __name__ == '__main__':
process_data("config.txt")
The finally block executes regardless of exception occurrence, making it ideal for resource cleanup.
The Else Clause
The else block executes exclusively when the try block completes without exception:
def calculate_metrics(data):
try:
parsed = [int(x) for x in data.split(',')]
except ValueError:
print("Parsing failed: non-numeric input")
return None
else:
# Executes only if parsing succeeds
average = sum(parsed) / len(parsed)
print(f"Computed average: {average}")
return average
finally:
print("Metric calculation finished")
Generic Exception Handling
Omitting the exception type creates a catch-all handler (though specific exceptions are preferred):
def safe_operation():
try:
risky_calculation()
except:
print("An error occurred")
Explicit Exception Raising
Use raise to trigger exceptions programmatically, similar to Java's throw:
def authenticate_user(credentials):
try:
user = lookup(credentials['username'])
if not verify_hash(credentials['password'], user.hash):
raise PermissionError("Invalid credentials")
except KeyError as err:
raise ValueError(f"Missing field: {err}") from err
else:
return create_session(user)
The from syntax preserves exception chaining, maintaining traceback context.
Custom Exception Hierarchies
Define domain-specific exceptions by inheriting from Exception or its subclasses:
class ServiceError(Exception):
"""Base exception for service layer"""
def __init__(self, code, message):
self.code = code
self.message = message
super().__init__(f"[{code}] {message}")
class ValidationError(ServiceError):
"""Input validation failures"""
pass
class TimeoutError(ServiceError):
"""Service timeout conditions"""
pass
def execute_request(payload):
if not validate_schema(payload):
raise ValidationError(400, "Schema mismatch")
try:
response = external_call(payload)
except ConnectionTimeout:
raise TimeoutError(503, "Service unavailable") from None
Standard convention appends "Error" to exception names. For modules with multiple error conditions, establish a base exception class with specific subclasses for granular error differentiation.