Python Custom Exceptions

Learn how to create custom exceptions in Python by subclassing Exception, raise custom exceptions with messages, catch them, add attributes, chain exceptions, and follow best practices.

Published

Updated

Read time 7 min read

Reviewed byDeepak Prasad

Python Custom Exceptions

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.

python
class InvalidAgeError(Exception):
    pass

assert issubclass(InvalidAgeError, Exception)
print("Defined:", InvalidAgeError.__name__)
Output

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.

python
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)
Output

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.

python
class InvalidAgeError(Exception):
    pass

try:
    age = -1
    if age < 0:
        raise InvalidAgeError("Age cannot be negative")
except InvalidAgeError as error:
    print(error)
Output

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.

python
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)
Output

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.

python
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)
Output

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.

python
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))
Output

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.

python
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)
Output

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, not BaseException. User code should not subclass BaseException for normal errors—that tier is for SystemExit, KeyboardInterrupt, and similar (exception hierarchy).
  • Use clear names such as InvalidConfigError or PaymentFailedError. Built-ins overwhelmingly use the Error suffix (ValueError, TypeError, FileNotFoundError); use Error or Exception in 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 Exception subclasses.
  • 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 (ValueError for bad values, FileNotFoundError for 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 as KeyError on 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 except clauses.
  • 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 BaseException for application errors, which can interfere with normal interpreter exit and keyboard interrupt handling.
  • raise "oops" (raising a string)—invalid in Python 3; always raise a BaseException instance, almost always an Exception subclass.
  • 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 FileExistsError for mkdir.

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.


References


Frequently Asked Questions

1. What should custom exceptions inherit from in Python?

Subclass Exception (or another Exception subclass); do not subclass BaseException for normal library errors—that level is reserved for system-exiting types like SystemExit and KeyboardInterrupt.

2. How do you add a message to a custom exception?

Pass the message to super().init(message) in init, optionally store structured fields on self before or after that call.

3. What does raise NewError(...) from old_error do?

It sets exception chaining so traceback shows that NewError was raised while handling old_error; use it when wrapping a lower-level failure in a domain-specific error.

4. When should I not define a custom exception?

When a built-in such as ValueError or FileNotFoundError already describes the failure clearly, or when the condition is ordinary control flow rather than an error.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …