Question

How to Declare Custom Exceptions in Modern Python

pythonpython-3.xexceptionpython-dataclassespep352

Question

How can I declare custom exception classes in modern Python?

I want to follow the same conventions used by built-in exceptions so that, for example, any extra message I include is displayed correctly when the exception is caught and printed.

By modern Python, I mean code that works in Python 2.5 while also following the style that is correct for Python 2.6 and Python 3.x.

By custom exception, I mean an exception type that can store extra information about the error, such as a message string and possibly another arbitrary object related to the failure.

I ran into this deprecation warning in Python 2.6.2:

class MyError(Exception):
    def __init__(self, message):
        self.message = message

MyError("foo")

This produces:

DeprecationWarning: BaseException.message has been deprecated as of Python 2.6

It seems that BaseException gives special meaning to an attribute named message. From PEP 352, I understand that this attribute had special behavior in Python 2.5 and was later deprecated, so it appears that using message as a custom attribute name is no longer a good idea.

I also know that Exception has an args attribute, but I have never been sure how it is meant to be used, or whether it is still the recommended approach in Python 3.

What is the correct way to define custom exceptions so they behave like standard exceptions and can also store extra data?

Short Answer

By the end of this page, you will understand how to create custom exceptions in Python by subclassing Exception, passing error text to the base class correctly, and optionally storing extra context safely. You will also learn how args, __str__, and custom attributes work, and how to write exception classes that behave like built-in exceptions.

Concept

In Python, custom exceptions are usually created by subclassing Exception or one of its subclasses.

The key idea is this:

  • An exception is just a class
  • Raising an exception means creating an instance of that class
  • Built-in exception behavior depends partly on the base class storing constructor values in args

A correct custom exception should usually do two things:

  1. Inherit from Exception
  2. Call Exception.__init__(...) with the message or values you want the exception to display

For example:

class MyError(Exception):
    pass

This is already a valid custom exception.

You can raise it like this:

raise MyError("Something went wrong")

Python stores the supplied values in exception.args. When the exception is printed, Python usually uses those values to build the string representation.

That is why calling the parent constructor matters.

Why message caused a warning

In older Python versions, some exceptions exposed a special .message attribute. That behavior was deprecated. So if you assign your own self.message, Python 2.6 may warn you because that name had special meaning.

Instead of relying on self.message, do one of these:

  • pass the message to Exception.__init__
  • store your own extra attributes under other names such as reason, code, details, or payload

Why this matters in real programming

Custom exceptions help you:

  • distinguish one type of failure from another
  • attach useful debugging or recovery data
  • write clearer error handling
  • communicate intent in larger codebases

For example, a program can treat ValidationError differently from DatabaseError, even if both contain text messages.

That makes code easier to debug and maintain.

Mental Model

Think of an exception like a labeled emergency report.

  • The exception class is the category of the report, like ValidationError or FileFormatError
  • The message is the human-readable description of what went wrong
  • The extra attributes are attached evidence, like a filename, record ID, or bad value

Built-in exceptions already know how to print the report summary because they store the main message in a standard place: args.

So when you create your own exception, the safest pattern is:

  • put the printable message into the base Exception
  • put any extra information into your own attributes

That way, Python and other tools can still print the error naturally, while your code can access more detailed context when needed.

Take Quiz

Syntax and Examples

Basic custom exception

class MyError(Exception):
    pass

raise MyError("Something went wrong")

This works because Exception already knows how to store the message in args.

Custom exception with extra data

class MyError(Exception):
    def __init__(self, message, payload=None):
        super(MyError, self).__init__(message)
        self.payload = payload

Usage:

try:
    raise MyError("Invalid input", payload={"field": "age", "value": -1})
except MyError as e:
    print(str(e))
    print(e.payload)

Output:

Invalid input
{'field': 'age', 'value': -1}

Why this pattern is good

class ValidationError(Exception):
    def __init__(self, field, value):
        message = "Invalid value for field %r: %r" % (field, value)
        super(ValidationError, self).__init__(message)
        self.field = field
        self.value = value

This gives you:

  • a readable message when printed
  • structured data for programmatic handling

Example:

try:
    raise ValidationError("age", -1)
except ValidationError as e:
    print(e)
    print(e.field)
    print(e.value)

Minimal but correct pattern

If all you need is a distinct exception type, keep it simple:

class ConfigError(Exception):
    pass

Only add __init__ when you actually need extra data or custom message construction.

Step by Step Execution

Consider this example:

class ValidationError(Exception):
    def __init__(self, field, value):
        message = "Invalid value for %s: %r" % (field, value)
        super(ValidationError, self).__init__(message)
        self.field = field
        self.value = value

try:
    raise ValidationError("age", -1)
except ValidationError as e:
    print("message:", str(e))
    print("args:", e.args)
    print("field:", e.field)
    print("value:", e.value)

What happens step by step

  1. Python defines the class ValidationError.

  2. raise ValidationError("age", -1) creates a new exception object.

  3. Inside __init__, the code builds this message:

    "Invalid value for age: -1"
    
  4. super(ValidationError, self).__init__(message) passes that message to Exception.

  5. The base class stores it in:

    e.args == ("Invalid value for age: -1",)
    
  6. The custom attributes are also stored:

    e.field == "age"
    e.value == -1
    
  7. The except ValidationError as e block catches the exception.

  8. str(e) prints the message stored through the base class.

Result

message: Invalid value for age: -1
args: ('Invalid value for age: -1',)
field: age
value: -1

This shows the core pattern:

  • user-friendly message goes through Exception
  • extra structured data lives in your own attributes

Real World Use Cases

Custom exceptions are useful whenever your program needs to express specific failure types clearly.

Input validation

class ValidationError(Exception):
    pass

Used when a user enters invalid data in a form or API request.

File and configuration loading

class ConfigError(Exception):
    pass

Used when a config file is missing required keys or contains invalid values.

API clients

class ApiError(Exception):
    def __init__(self, message, status_code=None):
        super(ApiError, self).__init__(message)
        self.status_code = status_code

Useful when you want to store the HTTP status code along with the message.

Data processing pipelines

class RecordFormatError(Exception):
    def __init__(self, message, record=None):
        super(RecordFormatError, self).__init__(message)
        self.record = record

Helpful when one bad row in a CSV or JSON dataset fails validation.

Business rules

Examples:

  • InsufficientFundsError
  • PermissionDeniedError
  • OrderStateError

These make application logic more readable than using generic Exception everywhere.

Take Quiz

Real Codebase Usage

In real projects, developers usually organize exceptions around domains and use them to support clear error handling.

Common pattern: base application exception

class AppError(Exception):
    pass

class ValidationError(AppError):
    pass

class DatabaseError(AppError):
    pass

This lets you catch all application-specific problems with one base class:

try:
    do_work()
except AppError as e:
    print("Application error:", e)

Pattern: add structured context

class ApiError(Exception):
    def __init__(self, message, status_code, response_body=None):
        super(ApiError, self).__init__(message)
        self.status_code = status_code
        self.response_body = response_body

This is common in API wrappers and service integrations.

Pattern: guard clauses

def create_user(username):
    if not username:
        raise ValidationError("username cannot be empty")

Exceptions often appear in early validation checks.

Pattern: wrapping lower-level errors

def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except IOError:
        raise ConfigError("Could not read config file: %s" % path)

This gives callers a domain-specific error instead of a low-level one.

Pattern: exception hierarchies

class PaymentError(Exception):
    pass

class CardDeclinedError(PaymentError):
    pass

class FraudCheckError(PaymentError):
    pass

This helps callers decide whether to catch one specific issue or a broader category.

Practical advice

In real codebases:

  • keep exception names specific
  • inherit from Exception, not directly from BaseException
  • store useful context in attributes
  • send the human-readable message to the base class
  • avoid overengineering exception classes if a simple subclass is enough

Common Mistakes

1. Forgetting to call the base class constructor

Broken:

class MyError(Exception):
    def __init__(self, message):
        self.text = message

Problem:

  • str(e) may not behave as expected
  • args will not contain the message

Better:

class MyError(Exception):
    def __init__(self, message):
        super(MyError, self).__init__(message)
        self.text = message

2. Using message as a custom attribute in old Python versions

Problematic in Python 2.6:

class MyError(Exception):
    def __init__(self, message):
        self.message = message

This can trigger deprecation warnings because .message had special handling.

Better:

class MyError(Exception):
    def __init__(self, message):
        super(MyError, self).__init__(message)
        self.details = message

3. Inheriting from BaseException

Broken:

class MyError(BaseException):
    pass

Why avoid this:

  • BaseException is for special system-level exceptions like SystemExit and KeyboardInterrupt
  • normal application errors should inherit from Exception

Better:

class MyError(Exception):
    pass

4. Writing a custom __str__ when you do not need one

Unnecessary:

class MyError(Exception):
    def __init__(self, message):
        super(MyError, self).__init__(message)

    def __str__(self):
        return self.args[0]

The base class already does this well in simple cases.

5. Storing only text when structured data would help

Less useful:

raise Exception("Bad input: field=age value=-1")

Better:

class ValidationError(Exception):
    def __init__(self, field, value):
        message = "Bad input for %s" % field
        super(ValidationError, self).__init__(message)
        self.field = field
        self.value = value

Now code can inspect e.field and e.value directly.

6. Catching overly broad exceptions

Risky:

try:
    do_work()
except Exception:
    print("Something failed")

Better when possible:

try:
    do_work()
except ValidationError as e:
    print(e)

Specific exception types are one of the main reasons to create custom exceptions in the first place.

Comparisons

Custom exception patterns compared

ApproachExampleGood forDownsides
Simple subclassclass MyError(Exception): passDistinct error type with no extra dataCannot store named context unless added later
Subclass with message passed to base classsuper(...).__init__(message)Standard printable behaviorSlightly more code
Subclass with custom attributesself.code = codeRich debugging and structured handlingMust design attributes clearly
Only store custom attributes, do not call base classself.text = messageAlmost never recommendedLoses standard exception behavior

Exception vs BaseException

ClassUse it?Why
ExceptionYesNormal application and library errors should inherit from this
BaseExceptionUsually noReserved for special interpreter-level exceptions

args vs custom attributes

FeaturePurposeExample
argsStandard storage used by base exception behavior('Bad input',)
Custom attributesNamed context for your own codefield='age', value=-1

Should you override __str__?

SituationRecommendation
One message stringUsually no
Message built from multiple attributesSometimes
Need very custom printed outputYes, if necessary

Most of the time, the best pattern is:

  • call Exception.__init__(message)
  • store extra values in named attributes
  • leave __str__ alone unless you need custom formatting
Take Quiz

Cheat Sheet

Quick pattern

class MyError(Exception):
    def __init__(self, message, data=None):
        super(MyError, self).__init__(message)
        self.data = data

Raise it

raise MyError("Something went wrong", data={"id": 123})

Catch it

try:
    do_work()
except MyError as e:
    print(e)        # human-readable message
    print(e.args)   # standard exception arguments
    print(e.data)   # custom extra data

Rules to remember

  • Inherit from Exception
  • Pass the printable message to the base class
  • Store extra context in your own attributes
  • Avoid using self.message in old Python compatibility code
  • Use simple subclasses when extra data is not needed
  • Only override __str__ if default printing is not enough

Minimal version

class ConfigError(Exception):
    pass

With structured data

class ValidationError(Exception):
    def __init__(self, field, value):
        message = "Invalid value for %s: %r" % (field, value)
        super(ValidationError, self).__init__(message)
        self.field = field
        self.value = value

Edge case

If you define __init__ and forget super(...).__init__(message), the exception may not print the message correctly.

FAQ

Should custom exceptions inherit from Exception or BaseException?

Use Exception. BaseException is meant for special built-in exceptions such as SystemExit and KeyboardInterrupt.

How do I include a message in a custom Python exception?

Pass the message to the parent constructor:

super(MyError, self).__init__(message)

This stores the text in args so it prints naturally.

Can a custom exception store extra data?

Yes. Add your own attributes such as code, filename, field, or payload.

Should I use self.message in a custom exception?

Avoid it, especially in Python 2.6 compatibility code, because .message had deprecated special behavior.

What is args on an exception?

args is a tuple of the values passed to the base exception constructor. Python often uses it when converting the exception to a string.

Do I need to override __str__ in a custom exception?

Usually no. If you pass a normal message to Exception, the default string output is already fine.

What is the simplest valid custom exception?

class MyError(Exception):
    pass

This is enough if you only need a distinct exception type.

Can I support Python 2.5 and still follow modern style?

Yes. A common compatible pattern is:

class MyError(Exception):
    def __init__(self, message, data=None):
        super(MyError, self).__init__(message)
        self.data = data
Take Quiz

Mini Project

Description

Build a small input validation module that checks user registration data and raises custom exceptions when values are invalid. This demonstrates how to define readable exception classes, attach extra context, and handle specific failures cleanly.

Goal

Create a validator that raises custom exceptions with both a human-readable message and structured data about the invalid field.

Requirements

  • Create a custom exception class for validation errors
  • Store a printable message in the base exception
  • Store the invalid field name and value as custom attributes
  • Write a function that validates a username and age
  • Raise the custom exception when validation fails
  • Catch the exception and print both the message and extra data
Take Quiz

Keep learning

Related questions

View more