Custom exception classes
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 then have all specific errors inherit from it. This allows a caller to catch `MyAppError` to handle all domain-specific errors while letting built-in system errors pass through. When wrapping a built-in error with a custom exception, you can accidentally swallow the original traceback if you don't use the `from` keyword: try: 1 / 0 except ZeroDivisionError as e: raise CalculationError("Math failed") # Original traceback is lost! This is called exception chaining, which preserves the original cause. It is covered fully in `exception-chaining`.
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.
- Swallowing the original traceback: When catching a standard error to raise a custom one, failing to use `raise CustomError from e` will hide the original failure context, making debugging very difficult.
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`)?
- What happens if you raise a custom exception inside an `except` block without using the `from e` syntax?
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
Add Context to a Custom Exception
Modify the `APIError` exception to accept a `status_code` argument in its `__init__`, save it as an instance attribute, and call `super().__init__(message)`. Then modify `fetch_data` to raise `APIError` with the message `'Not Found'` and `status_code` `404` when the `id` is not found.
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 with specific exception types
Previous: Raising exceptions with raise