Python ships with more than sixty built-in exception types, but none of them can describe problems specific to your application. python custom exceptions fill that gap — this article covers defining, raising, enriching with extra data, and organizing your own exception types into a coherent hierarchy.
Introduction: Why Build Your Own Exceptions
The limitation of built-in exceptions
Built-in exceptions like ValueError, TypeError, and KeyError cover general-purpose problems well, but they can't express anything specific to your application's actual domain. Consider a banking application: "salary out of an acceptable range" or "account balance too low to complete a withdrawal" are meaningful, specific problems — but there's no LowBalanceError or SalaryNotInRangeError built into Python, because Python has no idea what a "balance" or a "salary" even means in your program.
The benefits of defining your own
Custom exceptions offer two genuine, practical benefits. First, readability: raise LowBalanceError(...) immediately communicates what went wrong, far more clearly than a generic raise ValueError(...) that could mean almost anything depending on where it's caught. Second, precise catching: code that calls your function can catch your specific, meaningful exception type separately from unrelated bugs — except LowBalanceError: catches exactly the situation you defined, without also accidentally swallowing an unrelated ValueError that might indicate a genuine, unrelated programming mistake elsewhere in the same block.
What this article covers
Defining a basic custom exception, raising and catching it, enriching it with custom attributes and behavior, and organizing multiple custom exceptions into a coherent hierarchy — mirroring how Python's own standard library structures its built-in exceptions.
Defining a Basic Custom Exception
The minimal pattern
A custom exception is simply a class that inherits from Exception (or one of its subclasses) — often needing nothing more than pass, or a docstring describing when it's raised:
class InvalidAgeError(Exception):
"""Raised when an age value falls outside a valid range."""
passThat's a complete, fully functional custom exception. Inheriting from Exception automatically gives it everything a normal exception needs — the ability to carry a message, be raised, and be caught — without you needing to implement any of that machinery yourself.
Naming convention: the Error suffix
By strong, near-universal convention, custom exceptions are named with an Error suffix — InvalidAgeError, LowBalanceError, ConfigValidationError. This makes them instantly recognizable as exception types at a glance, consistent with the naming pattern used throughout Python's own built-in exceptions (ValueError, TypeError, KeyError, and so on).
Important: always inherit from Exception, never BaseException directly
This is genuinely important guidance, worth internalizing early: always inherit from Exception, never directly from BaseException.
class InvalidAgeError(Exception): # correct
pass
class InvalidAgeError(BaseException): # avoid this
passBaseException sits above Exception in Python's exception hierarchy, and it includes things like KeyboardInterrupt and SystemExit — signals that shouldn't be caught by normal, everyday error-handling code, exactly as covered in the earlier try/except/finally article. Inheriting from BaseException directly means your custom exception could accidentally slip past a standard except Exception: catch-all, behaving inconsistently with every other exception in your program. Exception is, for all practical purposes, always the correct base class for a custom exception.
Raising and Catching the Custom Exception
Basic raise and catch
class InvalidAgeError(Exception):
pass
def set_age(age):
if age < 0:
raise InvalidAgeError("Age cannot be negative")
return age
try:
set_age(-5)
except InvalidAgeError as e:
print(f"Invalid age: {e}")
# Invalid age: Age cannot be negativeraise InvalidAgeError("Age cannot be negative") works exactly like raising any built-in exception — the message passed to the constructor becomes accessible when the exception is caught, printed automatically as part of the exception's string representation.
A practical example: divide()
class DivisionError(Exception):
"""Raised when a division operation cannot be completed safely."""
pass
def divide(a, b):
if b == 0:
raise DivisionError(f"Cannot divide {a} by zero")
return a / b
try:
result = divide(10, 0)
except DivisionError as e:
print(f"Division failed: {e}")Instead of letting Python's built-in ZeroDivisionError leak through directly, divide() raises its own, more specific DivisionError — giving the caller a type that's meaningful specifically to this function's own domain, rather than a generic error that could originate from anywhere in the standard library.
A brief note on exception chaining with from
When you raise a new exception from inside an except block — a common pattern when translating a low-level error into a more meaningful, higher-level one — Python automatically shows both exceptions in the traceback, but you can make that relationship explicit with raise ... from ...:
class ConfigError(Exception):
pass
def load_setting(config, key):
try:
return config[key]
except KeyError as e:
raise ConfigError(f"Missing required setting: {key}") from e
load_setting({}, "api_key")KeyError: 'api_key'
The above exception was the direct cause of the following exception:
ConfigError: Missing required setting: api_keyfrom e explicitly marks the original KeyError as the direct cause of the new ConfigError, producing a clearer, more informative traceback than simply raising the new exception on its own — genuinely useful for debugging, since it preserves the original, lower-level error alongside your own more meaningful one, rather than discarding that original context entirely.
Adding Custom Attributes and Overriding init
Overriding init for extra context
Custom exceptions can carry more than just a message — overriding __init__ lets you attach whatever extra context is genuinely useful, like the specific invalid value that triggered the error:
class SalaryNotInRangeError(Exception):
def __init__(self, salary, min_salary, max_salary):
self.salary = salary
self.min_salary = min_salary
self.max_salary = max_salary
message = f"Salary {salary} is not within the allowed range ({min_salary}-{max_salary})"
super().__init__(message)
def set_salary(salary):
if not (30000 <= salary <= 150000):
raise SalaryNotInRangeError(salary, 30000, 150000)
return salary
try:
set_salary(20000)
except SalaryNotInRangeError as e:
print(e) # Salary 20000 is not within the allowed range (30000-150000)
print(e.salary) # 20000 — the actual offending value, accessible directlyCalling super().__init__(message) is important here — it ensures the exception is still fully functional as a normal exception (printable, correctly formatted in tracebacks), exactly the same principle behind calling super().__init__() in any subclass's constructor, covered in the earlier inheritance article. Without that call, str(e) and the default traceback output would be empty or unhelpful, even though e.salary would still technically work.
Overriding str for custom display
You can further customize exactly how the exception displays when printed or logged, using __str__, exactly as covered in the earlier magic methods article:
class LowBalanceError(Exception):
def __init__(self, balance, required):
self.balance = balance
self.required = required
super().__init__(f"Insufficient balance: have {balance}, need {required}")
def __str__(self):
shortfall = self.required - self.balance
return f"Balance too low by {shortfall} (have {self.balance}, need {self.required})"
try:
raise LowBalanceError(50, 200)
except LowBalanceError as e:
print(e) # Balance too low by 150 (have 50, need 200)This gives you full control over the exact wording shown when the exception is printed or logged, independent of whatever message was originally passed to super().__init__() — useful when you want the displayed message to include a derived value (like shortfall here) that wasn't part of the original constructor arguments directly.
Building an Exception Hierarchy
A common base class
For any application with more than one or two custom exceptions, it's genuinely worth defining a shared base class that all of your application-specific exceptions inherit from:
class AppError(Exception):
"""Base class for all custom exceptions in this application."""
pass
class InvalidAgeError(AppError):
pass
class LowBalanceError(AppError):
pass
class ConfigError(AppError):
passThis mirrors exactly how Python's own standard library organizes its built-in exceptions — FileNotFoundError, PermissionError, and several others all inherit from a common OSError base class, letting code catch broadly (except OSError:) or narrowly (except FileNotFoundError:) depending on what a given situation actually calls for.
Why this matters
With a shared AppError base class in place, calling code gains real flexibility:
try:
risky_app_operation()
except InvalidAgeError:
print("Specifically handle invalid age")
except AppError:
print("Handle any other application-specific error")
except Exception:
print("Handle a genuinely unexpected error")This lets you catch a specific exception type when you have a specific, meaningful response to it, while still being able to catch any application-specific error broadly (via AppError) as a fallback — distinctly separate from a final, genuinely broad except Exception: catching truly unexpected bugs that aren't part of your application's defined error vocabulary at all.
Best practices
Keep custom exceptions in a dedicated module for any project of meaningful size — a single exceptions.py file, importable from anywhere else in your codebase:
# exceptions.py
class AppError(Exception):
"""Base class for all custom exceptions in this application."""
pass
class InvalidAgeError(AppError):
"""Raised when a provided age value falls outside the valid range."""
pass
class LowBalanceError(AppError):
"""Raised when an account balance is insufficient for a requested operation."""
pass# elsewhere in your codebase
from exceptions import InvalidAgeError, LowBalanceErrorCentralizing every custom exception in one dedicated module makes them easy to find, easy to import consistently, and gives you one clear place to see your entire application's defined error vocabulary at a glance.
Document each exception's docstring, explaining exactly when it's meant to be raised. A one-line docstring, exactly as shown above, costs almost nothing to write and saves real time for anyone (including future you) trying to understand exactly what condition a given exception represents, without needing to hunt down and read every place it's actually raised throughout the codebase.