Question
How to Declare Custom Exceptions in Modern Python
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:
- Inherit from
Exception - 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, orpayload
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
ValidationErrororFileFormatError - 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.
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
-
Python defines the class
ValidationError. -
raise ValidationError("age", -1)creates a new exception object. -
Inside
__init__, the code builds this message:"Invalid value for age: -1" -
super(ValidationError, self).__init__(message)passes that message toException. -
The base class stores it in:
e.args == ("Invalid value for age: -1",) -
The custom attributes are also stored:
e.field == "age" e.value == -1 -
The
except ValidationError as eblock catches the exception. -
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:
InsufficientFundsErrorPermissionDeniedErrorOrderStateError
These make application logic more readable than using generic Exception everywhere.
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 fromBaseException - 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 expectedargswill 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:
BaseExceptionis for special system-level exceptions likeSystemExitandKeyboardInterrupt- 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
| Approach | Example | Good for | Downsides |
|---|---|---|---|
| Simple subclass | class MyError(Exception): pass | Distinct error type with no extra data | Cannot store named context unless added later |
| Subclass with message passed to base class | super(...).__init__(message) | Standard printable behavior | Slightly more code |
| Subclass with custom attributes | self.code = code | Rich debugging and structured handling | Must design attributes clearly |
| Only store custom attributes, do not call base class | self.text = message | Almost never recommended | Loses standard exception behavior |
Exception vs BaseException
| Class | Use it? | Why |
|---|---|---|
Exception | Yes | Normal application and library errors should inherit from this |
BaseException | Usually no | Reserved for special interpreter-level exceptions |
args vs custom attributes
| Feature | Purpose | Example |
|---|---|---|
args | Standard storage used by base exception behavior | ('Bad input',) |
| Custom attributes | Named context for your own code | field='age', value=-1 |
Should you override __str__?
| Situation | Recommendation |
|---|---|
| One message string | Usually no |
| Message built from multiple attributes | Sometimes |
| Need very custom printed output | Yes, 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
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.messagein 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
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
Keep learning
Related questions
@staticmethod vs @classmethod in Python Explained
Learn the difference between @staticmethod and @classmethod in Python with clear examples, use cases, mistakes, and a mini project.
Add Rows to a Pandas DataFrame in Python
Learn how to add rows to a Pandas DataFrame, why repeated row appends are slow, and when to use loc, concat, or record lists.
Call a Function by Name in a Python Module
Learn how to call a function by name in a Python module using strings, getattr, and safe patterns for dynamic function dispatch.