Python Basics — Raising Exceptions
Raising Exceptions
So far, you’ve been catching exceptions that Python raises automatically. But you can also raise your own exceptions to signal that something went wrong in your code.
The raise Statement
raise ValueError("Age cannot be negative")
This immediately stops execution and creates an exception that propagates up until it’s caught by a try/except block — or crashes the program.
Basic Usage
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age seems unrealistic")
return age
# Works fine
print(set_age(25)) # 25
# Raises exception
try:
set_age(-5)
except ValueError as e:
print(f"Invalid: {e}")
# Invalid: Age cannot be negative
When to Raise vs When to Return
Use raise When:
# Input is fundamentally wrong — caller needs to know
def withdraw(account, amount):
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > account.balance:
raise ValueError(f"Insufficient funds: {account.balance} available")
account.balance -= amount
return account.balance
Use a return value When:
# Expected outcome — not an error condition
def find_user(users, name):
for user in users:
if user["name"] == name:
return user
return None # Not found is normal, not exceptional
Decision Guide
| Situation | Use |
|---|---|
| Invalid input that should never happen | raise |
| Violated precondition or contract | raise |
| Missing item in a search | Return None or empty |
| Operation might fail normally | Return success/failure flag |
| Data integrity violation | raise |
Raising Built-in Exceptions
Choose the right exception type to communicate intent:
# Wrong type
def greet(name):
if not isinstance(name, str):
raise TypeError(f"Expected str, got {type(name).__name__}")
return f"Hello, {name}!"
# Invalid value
def set_volume(level):
if not 0 <= level <= 100:
raise ValueError(f"Volume must be 0-100, got {level}")
print(f"Volume set to {level}")
# Missing resource
def load_template(name):
templates = {"welcome": "Hello!", "bye": "Goodbye!"}
if name not in templates:
raise KeyError(f"Template '{name}' not found")
return templates[name]
# Not implemented
def process(data):
raise NotImplementedError("Subclasses must implement process()")
Re-Raising Exceptions
Sometimes you want to catch an exception, do something (like logging), and then let it propagate:
import logging
def process_data(data):
try:
result = data["value"] / data["divisor"]
except (KeyError, ZeroDivisionError) as e:
logging.error(f"Failed to process data: {e}")
raise # Re-raise the same exception
return result
The bare raise (no argument) re-raises the current exception with its original traceback intact.
Don’t Do This
# ❌ This loses the original traceback
except ValueError as e:
raise ValueError(str(e)) # Creates a NEW exception
# ✅ This preserves the original traceback
except ValueError:
raise # Re-raises the original
Raising with Custom Messages
def validate_email(email):
if not isinstance(email, str):
raise TypeError(f"Email must be a string, got {type(email).__name__}")
if "@" not in email:
raise ValueError(f"Invalid email format: '{email}' (missing @)")
if "." not in email.split("@")[1]:
raise ValueError(f"Invalid email domain: '{email}'")
return True
# Test
try:
validate_email("invalid-email")
except ValueError as e:
print(f"Validation failed: {e}")
# Validation failed: Invalid email format: 'invalid-email' (missing @)
Exception Chaining (from)
When one exception causes another, use from to preserve the chain:
def parse_config(text):
try:
# Simulate parsing
pairs = text.split(",")
config = {}
for pair in pairs:
key, value = pair.split("=")
config[key.strip()] = value.strip()
return config
except ValueError as e:
raise RuntimeError("Failed to parse config") from e
# Test
try:
parse_config("key1=value1,bad_entry")
except RuntimeError as e:
print(f"Error: {e}")
print(f"Caused by: {e.__cause__}")
Output:
Error: Failed to parse config
Caused by: not enough values to unpack (expected 2, got 1)
Full traceback shows both exceptions:
ValueError: not enough values to unpack (expected 2, got 1)
The above exception was the direct cause of the following exception:
RuntimeError: Failed to parse config
Suppress Chaining with from None
def get_setting(config, key):
try:
return config[key]
except KeyError:
raise ValueError(f"Missing required setting: '{key}'") from None
# The original KeyError won't appear in the traceback
# Test
try:
get_setting({"debug": True}, "port")
except ValueError as e:
print(e) # Missing required setting: 'port'
Guard Clauses Pattern
Use raise early to validate inputs at the top of a function:
def create_user(name, age, email):
# Guard clauses — validate all inputs first
if not name or not name.strip():
raise ValueError("Name cannot be empty")
if not isinstance(age, int) or age < 0:
raise ValueError(f"Age must be a positive integer, got {age}")
if "@" not in email:
raise ValueError(f"Invalid email: {email}")
# Main logic — only reached if all inputs are valid
user = {
"name": name.strip(),
"age": age,
"email": email.lower(),
}
return user
# Test
try:
user = create_user("Alice", 30, "alice@example.com")
print(f"Created: {user}")
except ValueError as e:
print(f"Failed: {e}")
Summary
- Use
raiseto signal errors that callers must handle - Choose the right exception type:
ValueError,TypeError,KeyError, etc. - Use bare
raiseto re-raise exceptions (preserves traceback) - Use
fromfor exception chaining — shows cause and effect - Use
from Noneto suppress the original exception context - Prefer guard clauses to validate inputs early
- Raise exceptions for exceptional situations, not normal flow control
Next up: Custom Exceptions →