This guide is for you if you already know that Python signals errors with exceptions and you want a small, working pattern for your own types: define a class, raise it, except it, attach a message, optionally add fields, chain from another error, and stay consistent with the standard library. General try / except mechanics live in the Python try except tutorial; here the focus stays on user-defined exceptions as described in the official tutorial and errors docs.
Tested on: Python 3.13.3; kernel 6.14.0-37-generic.
Each Python block below is a self-contained standard-library snippet suitable for the in-page Run control. If you adapt an example to read real files from disk, use {run=false} on the opening fence when your Run backend disallows file I/O.
What is a custom exception in Python?
A custom exception is a class you define—almost always subclassing Exception (directly or indirectly)—so callers can raise and except a named, domain-specific failure (InvalidAgeError, PaymentFailedError) instead of overloading a generic built-in. That is the same idea as defining any other class: see Python class example for classes and attributes in general. The language treats an exception instance like any other object along the traceback: message, optional __cause__ / __context__, and optional fields you attach.
Create a simple custom exception
The smallest useful form is an empty body: behavior comes from Exception. The snippet below is a complete script: it defines the class and prints a line so the in-page Run control shows output on Cloudflare Pages.
class InvalidAgeError(Exception):
pass
assert issubclass(InvalidAgeError, Exception)
print("Defined:", InvalidAgeError.__name__)You should see Defined: InvalidAgeError. That is enough for raise InvalidAgeError(...) and except InvalidAgeError in larger programs.
Raise a custom exception
Raise with raise when a rule is violated. The message string is optional but helps logs and users. This block is a full top-to-bottom script (no hidden cells): define class, define helper, call it inside try / except, print the result.
class InvalidAgeError(Exception):
pass
def validate_age(age):
if age < 0:
raise InvalidAgeError("Age cannot be negative")
try:
validate_age(-1)
except InvalidAgeError as err:
print("raised:", err)You should see raised: Age cannot be negative, confirming raise inside normal code paths works like built-in exceptions.
Catch a custom exception
Handle it like any other exception type—match the class (or a parent class) in except. When one try must branch on several concrete types, use a tuple of exception classes in the same except clause.
class InvalidAgeError(Exception):
pass
try:
age = -1
if age < 0:
raise InvalidAgeError("Age cannot be negative")
except InvalidAgeError as error:
print(error)You should see Age cannot be negative printed from print(error) (the default string form of the exception).
Custom exception with message
Store a formatted message and pass it to Exception.__init__ via super().__init__(...). That keeps str(exc) and tracebacks consistent with built-ins such as ValueError, and matches how you initialize other classes with __init__ as in the Python constructor tutorial.
class InvalidEmailError(Exception):
def __init__(self, email):
message = f"Invalid email address: {email}"
super().__init__(message)
self.email = email
try:
raise InvalidEmailError("user-example.com")
except InvalidEmailError as error:
print(error)
print(error.email)You should see the formatted message and user-example.com from error.email.
Custom exception with extra attributes
Add attributes when callers need structured data (codes, ids) beyond the message. Still keep super().__init__(message) so the base message path works. Heavy logic usually belongs in ordinary Python functions rather than in large exception classes.
class RateLimitExceeded(Exception):
def __init__(self, retry_after_seconds: int):
super().__init__(f"Rate limited; retry after {retry_after_seconds}s")
self.retry_after_seconds = retry_after_seconds
try:
raise RateLimitExceeded(30)
except RateLimitExceeded as err:
print(err.retry_after_seconds)You should see 30 printed from the handler.
Create a custom exception hierarchy
Define a package- or module-wide base (AppError) and derive specific errors from it. Callers can catch AppError to handle anything from your layer, or narrow to InvalidConfigError. This mirrors normal inheritance; for method resolution and super() in wider OOP code, see Python super.
class AppError(Exception):
"""Base for errors raised by this application."""
class InvalidConfigError(AppError):
pass
class ServiceUnavailableError(AppError):
pass
def load():
raise InvalidConfigError("missing port")
try:
load()
except AppError as err:
print(type(err).__name__, str(err))You should see InvalidConfigError and the message. Hierarchy matches the usual pattern in the stdlib: specific types under broader bases.
Raise custom exception from another exception
Use raise NewError(...) from error when your domain error wraps a lower-level failure. Python records the link so tracebacks show that the new exception was a direct consequence of the old one (PEP 3134, raise statement). Wrapping FileNotFoundError is a typical case when open() or Path.read_text() fails.
The runnable example below simulates a missing file by raising FileNotFoundError directly so it works in the in-page Run control without touching the filesystem. On your machine, the inner try is usually code that reads a path from disk.
class ConfigError(Exception):
pass
try:
try:
raise FileNotFoundError("config.yaml missing")
except FileNotFoundError as error:
raise ConfigError("Could not load configuration file") from error
except ConfigError as err:
print(type(err.__cause__).__name__)
print(err)You should see FileNotFoundError printed from __cause__, then the ConfigError message—evidence that chaining preserved the original failure.
For snippets that call Path.read_text() or open() on real paths, add {run=false} on the opening Python fence when file I/O is not supported in your Run environment (for example Cloudflare Pages snippet runs).
Custom exception best practices
When you record failures in long-running services, the logging module usually carries the message and stack trace; custom exceptions still identify the error type for filters and alerts.
- Inherit from
Exception, notBaseException. User code should not subclassBaseExceptionfor normal errors—that tier is forSystemExit,KeyboardInterrupt, and similar (exception hierarchy). - Use clear names such as
InvalidConfigErrororPaymentFailedError. Built-ins overwhelmingly use theErrorsuffix (ValueError,TypeError,FileNotFoundError); useErrororExceptionin your names and stay consistent within a project—there is no need to force both words into every name. - Provide a small base for your package (
MyLibError) so consumers can catch one type if they want. - Add custom attributes only when callers need structured data beyond the message.
- Keep exception classes small; put behavior in ordinary functions or services, not in huge
Exceptionsubclasses. - Prefer
raise ... from ...when wrapping another exception so context is not lost. - Do not invent a custom type when a built-in already describes the situation (
ValueErrorfor bad values,FileNotFoundErrorfor a missing file).
When not to create a custom exception
Skip a new class when:
ValueError,TypeError,KeyError,FileNotFoundError, or another built-in already matches the failure. For key-driven data, a missing key is often clearer asKeyErroron a plain dict than a bespoke name.- The situation is local and no caller needs to distinguish this error type from similar ones.
- The type name would only repeat the message without adding meaning for
exceptclauses. - You would use exceptions for ordinary control flow—use return values, result types, or loops instead when the case is expected rather than exceptional.
Common mistakes with custom exceptions
- Subclassing
BaseExceptionfor application errors, which can interfere with normal interpreter exit and keyboard interrupt handling. raise "oops"(raising a string)—invalid in Python 3; always raise aBaseExceptioninstance, almost always anExceptionsubclass.- Defining dozens of nearly identical exception classes that nobody catches individually.
- Ordering
except Exception(or a broad base) before your specific type so the specific handler never runs. - Forgetting
super().__init__(message)in__init__, which breaks default message handling and some logging integrations. - Wrapping a low-level error without
from, so the original traceback is harder to relate to your domain error when debugging. - Adding custom types where a built-in would make APIs more familiar to other Python developers.
- Treating a path as a directory when a file already exists at the same path, which raises
FileExistsErrorformkdir.
Python custom exception quick reference table
| Task | Pattern |
|---|---|
| Declare | class MyError(Exception): pass or custom __init__ |
| Raise | raise MyError("message") |
| Catch | except MyError as err: |
| Message + fields | super().__init__(msg); self.field = ... |
| Wrap lower-level | raise DomainError("...") from err |
| Package base | class LibError(Exception): pass then subclass |
Summary
Define custom exceptions as Exception subclasses, raise them when domain rules fail, and catch them by type. Use super().__init__(message) for messages, add attributes when callers need structure, and use raise ... from ... to chain from built-in or library errors. Prefer clear Error names, a small hierarchy for your package, and built-in exceptions when they already describe the problem.

