Python raise and Custom Exceptions
Learn how to use Python's raise statement, chain exceptions with raise...from, and create custom exception classes for clear, maintainable error handling.
Python lets you do more than catch errors — you can also signal them deliberately with the raise statement and create your own exception types to represent domain-specific problems. This chapter builds on Python Try...Except and covers:
- The
raisestatement — raising built-in exceptions - Re-raising exceptions inside an
exceptblock - Exception chaining with
raise ... from - Creating custom exception classes
- Building an exception hierarchy for a real application
- The
assertstatement and when to use it
The raise Statement
The raise statement lets you throw an exception at any point in your code. The most common form passes an exception instance with a descriptive message:
raise ExceptionType("message")Use raise when your code detects a problem that the caller must handle. For example, a function that accepts an age should reject negative values immediately rather than silently proceeding:
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
return age
try:
set_age(-1)
except ValueError as e:
print(e)
# Output: Age cannot be negativeChoosing the Right Built-in Exception
Python's built-in exception types carry meaning. Picking the right one makes your API easier to understand and allows callers to handle different error categories separately.
| Exception | When to raise it |
|---|---|
ValueError | Argument has the right type but an invalid value (age = -1) |
TypeError | Argument has the wrong type (age = "old") |
KeyError | A required dictionary key is missing |
IndexError | A sequence index is out of range |
FileNotFoundError | A required file does not exist |
PermissionError | The process lacks the rights to perform an operation |
RuntimeError | A general runtime problem that doesn't fit a narrower type |
NotImplementedError | A method exists in a base class but must be overridden |
Raising ValueError for a wrong value is much more informative than raising a bare Exception, because callers can write except ValueError to handle exactly that case.
Re-raising an Exception
Sometimes you want to do something on an exception — log it, clean up a resource — and then let the same exception propagate to the caller unchanged. Call raise with no arguments inside an except block to re-raise the current exception:
def read_config(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
print(f"Warning: config file not found at {path}")
raise # re-raise the original FileNotFoundError
try:
read_config("missing.cfg")
except FileNotFoundError as e:
print(f"Caught: {e}")
# Output:
# Warning: config file not found at missing.cfg
# Caught: [Errno 2] No such file or directory: 'missing.cfg'Using bare raise preserves the original traceback, which makes debugging much easier than catching and re-raising e as a new exception.
Exception Chaining with raise ... from
When you catch one exception and raise a different one, Python automatically records the original exception as the context of the new one. You can make this relationship explicit — and meaningful — using raise NewException from original:
def load_data(path):
try:
with open(path) as f:
return f.read()
except OSError as e:
raise RuntimeError("Failed to load configuration") from e
try:
load_data("config.json")
except RuntimeError as e:
print(f"Error: {e}")
print(f"Caused by: {e.__cause__}")
# Output:
# Error: Failed to load configuration
# Caused by: [Errno 2] No such file or directory: 'config.json'When Python prints the traceback it shows both exceptions in order, making it clear that the RuntimeError was a direct consequence of the OSError. This is especially useful in library code where you want to translate low-level OS errors into higher-level domain errors without hiding the root cause.
Suppressing the Chain with raise ... from None
Occasionally the original exception is an implementation detail you do not want to expose. Pass None as the cause to hide it:
def fetch(url):
try:
raise ConnectionError("timeout")
except ConnectionError:
raise RuntimeError("Network unavailable") from None
try:
fetch("http://example.com")
except RuntimeError as e:
print(f"Error: {e}")
print(f"Cause hidden: {e.__cause__}")
# Output:
# Error: Network unavailable
# Cause hidden: NoneThe traceback will only show the RuntimeError. Use this sparingly — hiding the root cause makes debugging harder for library consumers.
Creating Custom Exception Classes
Built-in exceptions cover common programming errors, but they are too generic for domain problems. If your e-commerce application raises a plain ValueError when a payment fails, callers cannot distinguish that from a bad function argument. Custom exception classes solve this.
A custom exception is simply a class that inherits from Exception (or one of its subclasses):
class InsufficientFundsError(Exception):
"""Raised when a bank account has insufficient funds."""
def __init__(self, amount, balance):
self.amount = amount
self.balance = balance
super().__init__(
f"Cannot withdraw {amount}: balance is only {balance}"
)
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(amount, self.balance)
self.balance -= amount
return self.balance
account = BankAccount(100)
try:
account.withdraw(150)
except InsufficientFundsError as e:
print(e)
print(f"You tried to withdraw: {e.amount}")
print(f"Available balance: {e.balance}")
# Output:
# Cannot withdraw 150: balance is only 100
# You tried to withdraw: 150
# Available balance: 100Key points about this pattern:
super().__init__(message)sets the human-readable string returned bystr(e).- Extra attributes (
self.amount,self.balance) let callers access structured data from the exception, not just a string. - A clear docstring documents when the exception should be raised.
Building an Exception Hierarchy
Real applications often have many related error types. Grouping them under a shared base class lets callers catch either the specific error or the whole category:
class AppError(Exception):
"""Base class for all application errors."""
class ValidationError(AppError):
"""Raised when user input fails validation."""
class DatabaseError(AppError):
"""Raised when a database operation fails."""
def validate_username(name):
if len(name) < 3:
raise ValidationError(f"Username '{name}' is too short (min 3 chars)")
try:
validate_username("ab")
except ValidationError as e:
print(f"Validation failed: {e}")
except AppError as e:
print(f"Application error: {e}")
# Output:
# Validation failed: Username 'ab' is too short (min 3 chars)A caller that only wants to catch database errors can write except DatabaseError. A caller that wants to catch any problem from your library can write except AppError. This mirrors the design of Python's own exception hierarchy, where OSError groups FileNotFoundError, PermissionError, and several others.
Guidelines for Custom Exceptions
- Inherit from
Exception, notBaseException.BaseExceptionis the root of Python's hierarchy and also includesSystemExitandKeyboardInterrupt, which should not be caught accidentally. - End the class name in
Errorfor exceptions that signal a problem. This matches Python's own naming (ValueError,TypeError,IOError). - Keep the class minimal unless you need extra attributes. An empty body with a docstring is perfectly valid.
- Put exceptions in a dedicated module (e.g.,
exceptions.py) for larger projects so callers can import them without pulling in the rest of your code.
The assert Statement
assert is a lightweight way to express invariants — conditions that must be true for your code to be correct:
def divide(a, b):
assert b != 0, "Divisor must not be zero"
return a / b
try:
divide(10, 0)
except AssertionError as e:
print(f"AssertionError: {e}")
print(divide(10, 2))
# Output:
# AssertionError: Divisor must not be zero
# 5.0assert condition, message raises AssertionError with the given message when condition is False.
Important limitation: Python removes assert statements when run with the -O (optimize) flag. This means:
- Use
assertfor internal consistency checks and debugging aids only. - Use
raisewith a proper exception for user-facing input validation and public API checks that must always run.
Common Mistakes
Catching and silently ignoring exceptions
# Bad — the error disappears
try:
result = risky_operation()
except Exception:
pass
# Better — at minimum, log or re-raise
try:
result = risky_operation()
except Exception as e:
print(f"Operation failed: {e}")
raiseRaising a string instead of an exception
# Wrong — strings are not exceptions
raise "something went wrong" # TypeError
# Correct
raise ValueError("something went wrong")Catching BaseException accidentally
# Dangerous — this catches KeyboardInterrupt and SystemExit too
except BaseException:
...
# Use Exception instead
except Exception:
...Summary
| Technique | When to use it |
|---|---|
raise ExceptionType("msg") | Signal a problem the caller must handle |
raise (bare) | Re-raise the current exception after logging or cleanup |
raise NewError(...) from original | Translate a low-level error into a higher-level one, preserving the cause |
raise NewError(...) from None | Translate an error while hiding the internal cause |
| Custom exception class | Give domain-specific errors a unique, catchable type |
| Exception hierarchy | Allow callers to catch narrow or broad categories of errors |
assert | Verify internal invariants during development only |
For the full picture of catching and handling exceptions, see Python Try...Except. To understand how custom exceptions fit into class design, review Python Classes and Objects and Python Inheritance.