Skip to main content

Raise Exceptions: Signal Errors with raise Keyword

The raise keyword allows you to manually signal errors in your code, complementing Python's automatic exception handling. While Python raises exceptions like ValueError or FileNotFoundError automatically, you often need to enforce your own business logic rules—like preventing negative weights in a shipping function or insufficient funds in a bank account. The raise keyword lets you stop execution and alert the calling code that an invalid state has occurred, enabling you to build robust applications with clear error contracts.

Key Takeaways:

  • Use raise ExceptionType("message") to manually trigger exceptions
  • Raise built-in exceptions (ValueError, TypeError, KeyError) when they accurately describe your error
  • Create custom exception classes by inheriting from Exception for domain-specific errors
  • Provide clear, actionable error messages so developers understand what went wrong and how to fix it

What Is the raise Keyword and When Do You Use It?

The raise keyword manually triggers an exception at any point in your code. When Python encounters raise, it immediately stops normal execution and enters exception handling mode, looking for a matching except block to handle the error.

Basic syntax:

raise ExceptionType("A descriptive error message")

You can raise any object that inherits from Python's base Exception class.

Why Raise Exceptions?

Raising exceptions is essential for enforcing function contracts. A function has implicit rules: "this parameter must be positive," "this dictionary must contain a name key," or "this file must exist." When these rules are violated, raising an exception stops execution and alerts the caller that something is wrong.

Example: Input validation with raise

def calculate_shipping(weight_kg):
"""Calculate shipping cost. Weight must be positive."""
if weight_kg <= 0:
raise ValueError("Weight must be a positive number.")
return weight_kg * 2.50

# Valid call succeeds
cost = calculate_shipping(10) # Returns 25.0

# Invalid call raises and stops execution
cost = calculate_shipping(-5) # Raises ValueError immediately

How Do You Raise Built-in Exceptions?

Python provides dozens of built-in exception types. Always use the most appropriate one for your error condition, as this makes your code consistent with the Python ecosystem and helps calling code handle errors correctly.

Common Built-in Exceptions to Raise

  • ValueError — The argument has the correct type but an invalid value for your logic. (Example: negative weight)
  • TypeError — The argument type is wrong. (Example: passing a string when an integer is required)
  • KeyError — A dictionary or mapping lookup failed. (Example: accessing a missing key)
  • IndexError — A sequence (list, tuple) index is out of range.
  • AttributeError — An object doesn't have the requested attribute.
  • RuntimeError — A general error that doesn't fit other categories.

Practical Examples

Validating numeric input:

def calculate_shipping(weight_kg):
"""Calculate shipping cost. Weight cannot be zero or negative."""
if weight_kg <= 0:
raise ValueError("Weight must be a positive number.")

return weight_kg * 2.50

# Usage
try:
cost = calculate_shipping(10)
print(f"Cost: ${cost:.2f}") # Output: Cost: $25.00

cost_invalid = calculate_shipping(-5)
except ValueError as e:
print(f"Error: {e}") # Output: Error: Weight must be a positive number.

Ensuring type correctness:

def process_ages(ages):
"""Process a list of ages. Must receive a list."""
if not isinstance(ages, list):
raise TypeError(f"Expected a list, got {type(ages).__name__}")

return sum(ages) / len(ages) if ages else 0

# Valid call
avg = process_ages([18, 25, 30]) # Returns 24.33

# Invalid call raises TypeError
avg = process_ages("not a list") # Raises TypeError immediately

Checking for required dictionary keys:

def create_user(data):
"""Create a user from a dictionary. Requires 'name' and 'email' keys."""
if "name" not in data:
raise KeyError("Missing required key: 'name'")
if "email" not in data:
raise KeyError("Missing required key: 'email'")

return f"User {data['name']} created with email {data['email']}"

# Valid call
user = create_user({"name": "Alice", "email": "[email protected]"})

# Invalid call raises KeyError
user = create_user({"name": "Bob"}) # Raises KeyError: Missing required key: 'email'

How Do You Create Custom Exceptions?

Built-in exceptions cover common errors, but sometimes you need domain-specific exceptions that describe your application's unique error conditions. Custom exceptions improve code clarity and allow precise error handling.

To create a custom exception, define a class that inherits from Exception:

class YourCustomError(Exception):
"""A custom exception for your application."""
pass

Real-World Example: Banking Application

class InsufficientFundsError(Exception):
"""Raised when a withdrawal exceeds the account balance."""
pass

class AccountNotFoundError(Exception):
"""Raised when an account doesn't exist."""
pass

def withdraw(balance, amount):
"""Withdraw an amount from a balance."""
if amount > balance:
raise InsufficientFundsError(
f"Cannot withdraw ${amount:.2f}. Balance is only ${balance:.2f}."
)

print("Withdrawal successful.")
return balance - amount

# Usage
my_balance = 100.00

try:
print(f"Balance: ${my_balance:.2f}")

# First withdrawal succeeds
my_balance = withdraw(my_balance, 50.00)
print(f"New Balance: ${my_balance:.2f}")

# Second withdrawal fails
my_balance = withdraw(my_balance, 75.00)

except InsufficientFundsError as e:
print(f"Transaction failed: {e}")

Output:

Balance: $100.00
Withdrawal successful.
New Balance: $50.00
Transaction failed: Cannot withdraw $75.00. Balance is only $50.00.

Custom Exception with Additional Data

You can add custom attributes to exceptions for richer error information:

class ValidationError(Exception):
"""Raised when data validation fails."""
def __init__(self, message, field=None, value=None):
self.message = message
self.field = field
self.value = value
super().__init__(self.message)

def validate_email(email):
"""Validate email format."""
if "@" not in email or "." not in email:
raise ValidationError(
"Invalid email format",
field="email",
value=email
)
return email

# Usage
try:
validate_email("not-an-email")
except ValidationError as e:
print(f"Validation failed for {e.field}: {e.value}")
# Output: Validation failed for email: not-an-email

Best Practices for Raising Exceptions

1. Use Specific Exception Types

Always raise the most specific exception type that describes your error. Specific exceptions enable calling code to handle different errors differently:

# GOOD: Specific exceptions for different errors
def get_user(user_id):
if not isinstance(user_id, int):
raise TypeError("user_id must be an integer")

if user_id < 0:
raise ValueError("user_id must be non-negative")

if user_id not in database:
raise KeyError(f"User {user_id} not found")

return database[user_id]

# Calling code can handle different errors
try:
user = get_user(user_input)
except TypeError:
print("Invalid input type")
except ValueError:
print("Invalid input value")
except KeyError:
print("User not found")

2. Provide Clear, Actionable Error Messages

The message you pass to an exception is crucial. It should explain what went wrong and often hint at how to fix it:

# POOR: Vague message
raise ValueError("Invalid")

# GOOD: Clear, actionable message
raise ValueError(
"Age must be between 0 and 150. Received: -5"
)

3. Raise Exceptions Early

Check preconditions at the beginning of a function (fail-fast principle). This prevents silent failures deeper in code:

# GOOD: Validate inputs first, then process
def process_data(data, multiplier):
if not isinstance(data, (list, tuple)):
raise TypeError("data must be a list or tuple")

if not isinstance(multiplier, (int, float)):
raise TypeError("multiplier must be numeric")

if multiplier < 0:
raise ValueError("multiplier must be non-negative")

# Safe to process
return [x * multiplier for x in data]

4. Don't Swallow Exceptions Silently

Avoid catching exceptions and ignoring them:

# BAD: Silent failure
try:
result = risky_operation()
except Exception:
pass # What went wrong? No one knows!

# GOOD: Either handle or re-raise
try:
result = risky_operation()
except SpecificError as e:
print(f"Handled error: {e}")
result = default_value
except Exception:
raise # Re-raise unexpected errors

Key Takeaways

  • The raise Keyword: Use raise ExceptionType("message") to manually trigger exceptions and enforce your function's contracts.
  • Built-in Exceptions: Always prefer built-in exceptions (ValueError, TypeError, KeyError) when they accurately describe your error.
  • Custom Exceptions: Create custom exception classes for domain-specific errors by inheriting from Exception; this improves code clarity and error handling precision.
  • Clear Messages: Provide descriptive error messages that explain what went wrong and how to fix it.
  • Fail Fast: Check preconditions and raise exceptions early, before your function tries to process invalid data.

Frequently Asked Questions

What is the difference between raise and return?

return ends a function and passes a value back to the caller. raise ends a function abnormally, creating an exception that must be handled by a try-except block. Use return for normal results; use raise for errors that shouldn't be silently ignored.

Can you raise an exception without a message?

Yes. You can write raise ValueError() with no message, but this is bad practice. Always provide a message explaining what went wrong so developers understand the error.

Should you create a custom exception for every error condition?

No. Create custom exceptions only when a built-in exception doesn't accurately describe your error. If ValueError fits, use it. Custom exceptions are for domain-specific errors that built-in types don't cover (like InsufficientFundsError in banking).

What happens if you raise an exception in a function but don't catch it?

The exception propagates up the call stack. If no try-except block catches it, the program crashes and displays a traceback. This is often acceptable—it alerts developers that something went wrong. Only catch exceptions if your code can meaningfully handle them.

Can you raise an exception while already handling another exception?

Yes. This is called exception chaining. You can raise a new exception while handling another by using raise ... from ...:

try:
file = open("missing.txt")
except FileNotFoundError as e:
raise RuntimeError("Failed to load config") from e

Further Reading