Custom exceptions & EAFP
Overview
Custom exceptions are your own error classes created by inheriting from Python's built-in `Exception` class. By convention, their names end in `Error` (e.g. `UserNotFoundError`). You can add custom attributes to them, such as `status_code` or `user_id`, allowing the exception to carry structured data about the failure up to the caller. A common professional pattern is to create one base exception for your application (`class MyAppError(Exception): pass`), and have all specific errors inherit from it.
Built-in exceptions like `ValueError` are too generic. If your service catches a `ValueError`, did it happen because a user provided bad input, or because a database configuration was malformed? Custom exceptions create a domain-specific vocabulary. By raising `InsufficientFundsError`, the caller can catch EXACTLY that error and trigger a specific UI flow, without accidentally catching unrelated standard library errors.
Where used: Building SDKs so users can catch specific API errors, Domain-driven design to express business logic failures, Mapping specific exceptions to HTTP 400/404/500 responses in FastAPI
Why learn this
- Creating a clear error hierarchy for your application
- Attaching context (like an HTTP status code or a missing ID) directly to the error object
- Allowing callers to catch specific business errors while letting catastrophic system errors crash
Code walkthrough
class PaymentError(Exception):
def __init__(self, message, code):
super().__init__(message)
self.code = code
try:
raise PaymentError("Declined", code=402)
except PaymentError as e:
print(f"Failed: {e} (HTTP {e.code})")
Focus: The custom `__init__` allows the exception to carry a specific `code` attribute, turning the error into a structured data carrier.
Common mistakes
- Inheriting from BaseException: You should always inherit from `Exception`. Inheriting directly from `BaseException` means your custom error won't be caught by a standard `except Exception:` block, which usually breaks assumptions made by third-party frameworks.
- Forgetting to call super().__init__: If you define a custom `__init__` to accept extra arguments (like `status_code`), you must remember to call `super().__init__(message)` so the message is stored in `self.args` and shows up in the string representation.
Glossary
- inheriting
- A way for a new class to take on the properties and behaviors of an existing class, e.g. `class CustomError(Exception):`.
- traceback
- A detailed report showing the sequence of function calls that led to an error in your code, e.g. `Traceback (most recent call last):`.
Recall questions
- When creating a custom exception, which built-in class should you inherit from?
- What is the primary architectural benefit of creating a base exception class for your entire application (e.g. `class MyAppError(Exception): pass`)?
Understanding checks
What will be printed when this code is executed?
Caught AppError
Because `AuthError` inherits from `AppError`, the `except AppError` block matches the exception first. When catching exceptions, you must put the most specific subclasses before the more general base classes.
A junior developer wants to ensure their custom exception `FatalSystemError` cannot be caught by accidental `except Exception:` blocks, so they inherit from `BaseException`. What is the danger of this?
Inheriting from `BaseException` means typical `try/except Exception:` blocks won't catch it, potentially bypassing cleanup logic or global error handlers.
`BaseException` is the root of all exceptions in Python, intended only for system-exiting exceptions like `KeyboardInterrupt` or `SystemExit`. Inheriting from it breaks the assumption made by most frameworks and libraries that all standard errors subclass `Exception`.
Practice tasks
Challenge
Build a custom validation error
Create an `InvalidEmailError` that takes an invalid email string. Write `validate_email(email)` that raises it if there's no '@'. Catch it and print its message.
Continue learning
Previous: try / except / else / finally