Raising exceptions¶
In this tutorial, you will learn how to use the raise statement to signal errors in your own code. You will also create custom exception classes that make your code easier to understand and debug.
Time commitment: 15–20 minutes
Prerequisites:
- Completion of Exception types
- Understanding of
try/exceptblocks and built-in exception types
Learning objectives¶
By the end of this tutorial, you will be able to:
- Use the
raisestatement to signal errors in your functions - Choose the appropriate built-in exception type to raise
- Create custom exception classes by inheriting from
Exception - Add attributes and messages to custom exception classes
Why raise exceptions?¶
So far, you have learned how to handle exceptions that Python raises automatically. But what about errors that are specific to your own code?
For example, suppose you write a function that calculates a person's age. Negative ages do not make sense, so your function should signal an error when it receives one. This is where the raise statement comes in.
The raise statement¶
The raise statement lets you signal that an exceptional condition has occurred. You provide an exception object, and Python interrupts the normal flow of execution.
raise ExceptionType("A message describing the error")
Let us see it in practice.
def validate_age(age: int) -> int:
"""Validate that the given age is a non-negative integer.
Args:
age: The age value to validate.
Returns:
The validated age.
Raises:
ValueError: If the age is negative or greater than 150.
TypeError: If the age is not an integer.
"""
if not isinstance(age, int):
raise TypeError(f"Age must be an integer, got {type(age).__name__}")
if age < 0:
raise ValueError(f"Age must be non-negative, got {age}")
if age > 150:
raise ValueError(f"Age must be 150 or less, got {age}")
return age
# Valid age
print(validate_age(25))
# Invalid age: the exception is raised and can be handled
try:
validate_age(-5)
except ValueError as e:
print(f"Validation failed: {e}")
Notice how the error message clearly describes what went wrong. Good error messages make debugging much easier.
Choosing the right exception type¶
When raising exceptions, choose the built-in exception type that best describes the error:
| Use this exception | When |
|---|---|
ValueError |
The value is the right type but not acceptable |
TypeError |
The argument is the wrong type |
FileNotFoundError |
A required file does not exist |
PermissionError |
The program lacks permission for an operation |
RuntimeError |
An error that does not fit other categories |
NotImplementedError |
A method or feature is not yet implemented |
def calculate_average(numbers: list[float]) -> float:
"""Calculate the average of a list of numbers.
Args:
numbers: A list of numbers.
Returns:
The average value.
Raises:
TypeError: If the argument is not a list.
ValueError: If the list is empty.
"""
if not isinstance(numbers, list):
raise TypeError(f"Expected a list, got {type(numbers).__name__}")
if len(numbers) == 0:
raise ValueError("Cannot calculate the average of an empty list")
return sum(numbers) / len(numbers)
# Normal usage
print(calculate_average([10, 20, 30]))
# Empty list
try:
calculate_average([])
except ValueError as e:
print(f"ValueError: {e}")
# Wrong type
try:
calculate_average("not a list")
except TypeError as e:
print(f"TypeError: {e}")
Creating custom exception classes¶
Sometimes, built-in exception types are not specific enough for your needs. In these cases, you can create your own exception classes by inheriting from Exception.
A custom exception class is a regular Python class with one requirement: it must inherit from Exception (or one of its subclasses).
class InsufficientFundsError(Exception):
"""Raised when a withdrawal exceeds the available balance."""
pass
def withdraw(balance: float, amount: float) -> float:
"""Withdraw an amount from the balance.
Args:
balance: The current account balance.
amount: The amount to withdraw.
Returns:
The new balance after the withdrawal.
Raises:
InsufficientFundsError: If the amount exceeds the balance.
ValueError: If the amount is negative.
"""
if amount < 0:
raise ValueError("Withdrawal amount must be non-negative")
if amount > balance:
raise InsufficientFundsError(
f"Cannot withdraw {amount}: only {balance} available"
)
return balance - amount
# Normal withdrawal
print(withdraw(100.0, 30.0))
# Insufficient funds
try:
withdraw(50.0, 75.0)
except InsufficientFundsError as e:
print(f"Transaction declined: {e}")
Adding attributes to custom exceptions¶
Custom exception classes can store extra information by defining an __init__ method. This is useful when the exception handler needs details about what went wrong.
class ValidationError(Exception):
"""Raised when a validation check fails.
Attributes:
field: The name of the field that failed validation.
reason: A description of the validation failure.
"""
def __init__(self, field: str, reason: str) -> None:
self.field = field
self.reason = reason
super().__init__(f"Validation failed for '{field}': {reason}")
def validate_email(email: str) -> str:
"""Validate that a string looks like an email address.
Args:
email: The email address to validate.
Returns:
The validated email address.
Raises:
ValidationError: If the email address is not valid.
"""
if "@" not in email:
raise ValidationError("email", "must contain '@'")
if "." not in email.split("@")[1]:
raise ValidationError("email", "domain must contain '.'")
return email
# Valid email
print(validate_email("alice@example.com"))
# Invalid email: access the exception attributes
try:
validate_email("not-an-email")
except ValidationError as e:
print(f"Error: {e}")
print(f"Field: {e.field}")
print(f"Reason: {e.reason}")
The exception handler can access e.field and e.reason to understand exactly what went wrong, without having to parse the error message string.
Re-raising exceptions¶
Sometimes you want to handle an exception partially (for example, to log it) and then let it propagate to the caller. You can do this with a bare raise statement inside an except block.
def process_data(data: list[str]) -> list[int]:
"""Convert a list of strings to integers.
Args:
data: A list of numeric strings.
Returns:
A list of integers.
Raises:
ValueError: If any string cannot be converted to an integer.
"""
results = []
for item in data:
try:
results.append(int(item))
except ValueError:
print(f"Warning: could not convert {item!r}")
raise # Re-raise the exception to the caller
return results
try:
process_data(["1", "2", "abc"])
except ValueError as e:
print(f"Processing failed: {e}")
The function logged a warning and then re-raised the same exception so the caller could decide how to handle it.
def validate_temperature(celsius: float) -> float:
"""Validate that a temperature is above absolute zero."""
pass # Replace this with your implementation
Click to reveal the solution
def validate_temperature(celsius: float) -> float:
"""Validate that a temperature is above absolute zero."""
if celsius < -273.15:
raise ValueError(
f"Temperature cannot be below absolute zero (-273.15), got {celsius}"
)
return celsius
Exercise 2: Custom exception¶
Create a custom exception class called InvalidPasswordError with attributes password_length (an integer) and reason (a string). Then write a function called validate_password that raises InvalidPasswordError if the password is shorter than eight characters.
class InvalidPasswordError(Exception):
"""Raised when a password does not meet the requirements."""
pass # Replace this with your implementation
def validate_password(password: str) -> str:
"""Validate that a password meets minimum length requirements."""
pass # Replace this with your implementation
Click to reveal the solution
class InvalidPasswordError(Exception):
"""Raised when a password does not meet the requirements."""
def __init__(self, password_length: int, reason: str) -> None:
self.password_length = password_length
self.reason = reason
super().__init__(f"Invalid password (length {password_length}): {reason}")
def validate_password(password: str) -> str:
"""Validate that a password meets minimum length requirements."""
if len(password) < 8:
raise InvalidPasswordError(
password_length=len(password),
reason="must be at least 8 characters",
)
return password
Summary¶
In this tutorial, you learned the following:
- The
raisestatement lets you signal errors in your own code - Choose the appropriate built-in exception type for the error (for example,
ValueErrorfor bad values,TypeErrorfor wrong types) - You can create custom exception classes by inheriting from
Exception - Custom exceptions can carry extra attributes that help the exception handler respond to the error
- A bare
raiseinside anexceptblock re-raises the current exception
In the next tutorial, Cleanup with finally, you will learn how to use the finally and else clauses, and how context managers help with resource cleanup.