Skip to content

Custom Errors

Python’s built-in exceptions (ValueError, TypeError, RuntimeError, …) are generic. When your code raises a ValueError, callers cannot tell whether it came from their input, a broken config file, or a network response.

Custom exception classes give your errors a distinct type that callers can catch selectively, add structured attributes (like HTTP status codes or field names), and document what can go wrong in each part of your application.

The minimal custom exception is just a class that inherits from Exception:

class AppError(Exception):
"""Base class for all application errors."""

That is enough to raise and catch it by name:

raise AppError("something went wrong")

Override __init__ to attach extra context to the exception:

class AppError(Exception):
def __init__(self, message: str, code: int = 0) -> None:
super().__init__(message) # pass message to Exception
self.code = code

Callers can then read err.code to get structured information without parsing the message string.

Create subclasses for each distinct failure category. Every NotFoundError is also an AppError, so callers can catch at whichever level of specificity they need.

class ValidationError(AppError):
def __init__(self, field: str, message: str) -> None:
super().__init__(f'Validation failed on "{field}": {message}', code=400)
self.field = field
class NotFoundError(AppError):
def __init__(self, resource: str, resource_id: int) -> None:
super().__init__(f"{resource} id={resource_id} not found", code=404)
self.resource = resource
self.resource_id = resource_id

Catching at different levels:

try:
raise ValidationError("email", "must contain @")
except ValidationError as e:
print(f"Field {e.field!r} invalid: {e}") # specific
except AppError as e:
print(f"App error [{e.code}]: {e}") # broad

str(e) returns the message you passed to super().__init__(). This is what gets printed when you print(e) or include the exception in an f-string.

repr(e) returns ExceptionClass(message) — useful for logging.

# --- base application error ---
class AppError(Exception):
"""Base class for all application errors."""
def __init__(self, message: str, code: int = 0) -> None:
super().__init__(message)
self.code = code
# --- specific subclasses ---
class ValidationError(AppError):
"""Raised when input validation fails."""
def __init__(self, field: str, message: str) -> None:
super().__init__(
f'Validation failed on field "{field}": {message}',
code=400,
)
self.field = field
class NotFoundError(AppError):
"""Raised when a requested resource does not exist."""
def __init__(self, resource: str, resource_id: int) -> None:
super().__init__(
f"{resource} with id={resource_id} not found",
code=404,
)
self.resource = resource
self.resource_id = resource_id
# --- catching at different levels ---
errors = [
ValidationError("email", "must contain @"),
NotFoundError("User", 42),
AppError("generic app failure", code=500),
]
for err in errors:
try:
raise err
except ValidationError as e:
print(f"[{e.code}] Validation on {e.field!r}: {e}")
except NotFoundError as e:
print(f"[{e.code}] {e.resource} not found (id={e.resource_id})")
except AppError as e:
print(f"[{e.code}] AppError: {e}")
# --- isinstance hierarchy ---
print()
e = ValidationError("name", "too short")
print(f"ValidationError? {isinstance(e, ValidationError)}")
print(f"AppError? {isinstance(e, AppError)}")
print(f"Exception? {isinstance(e, Exception)}")
print(f"BaseException? {isinstance(e, BaseException)}")
Why should custom application exceptions subclass `Exception` instead of `BaseException`?
Given a hierarchy `NotFoundError -> AppError -> Exception`, which `except` clause would catch a `NotFoundError`?
What does calling `super().__init__(message)` inside a custom exception's `__init__` do?
You have `except ValidationError` before `except AppError` in a try block. A ValidationError is raised. Which clause runs?