Raising Exceptions
Sometimes you need to signal an error yourself - when validation fails, when
preconditions aren't met, or when something unexpected happens. The raise statement
lets you throw exceptions, create custom exception types for your application, and chain
exceptions to preserve error context!
Basic Raise Statement
Use raise ExceptionType("message") to throw an exception. Choose the appropriate
built-in exception type - ValueError for invalid values, TypeError for
wrong types, KeyError for missing keys. Always include a descriptive message.
Click Run to execute your code
ValueError - Right type, wrong value: raise ValueError("age must be positive")TypeError - Wrong type: raise TypeError("expected string")KeyError - Missing key: raise KeyError("config missing 'host'")RuntimeError - General runtime issue: raise RuntimeError("state corrupted")NotImplementedError - Subclass must override: raise NotImplementedError
Re-raising Exceptions
Sometimes you want to catch an exception, do something (like logging), and then let it
propagate. Use bare raise (without arguments) to re-raise the current exception.
This preserves the original traceback, which is crucial for debugging.
Click Run to execute your code
raise - Bare raise preserves original tracebackraise ValueError(...) - Creates NEW exception, loses original tracebackUse bare
raise when you want to log and continue propagating.
Use raise NewException() from original when converting exception types.
Custom Exception Classes
Create custom exceptions by inheriting from Exception. This lets you add
attributes, create exception hierarchies, and provide domain-specific error handling.
Always inherit from Exception, not BaseException.
Click Run to execute your code
- Always inherit from
Exception (not BaseException)- Call
super().__init__(message) in your __init__- Add useful attributes (error codes, field names, etc.)
- Create a base exception for your app:
class MyAppError(Exception)- Specific exceptions inherit from base:
class AuthError(MyAppError)
Exception Chaining
Exception chaining preserves the original error when wrapping it in a new exception.
Use raise NewException() from original to explicitly chain. Use
raise NewException() from None to suppress the chain (hide internal details).
Click Run to execute your code
raise X from Y - Explicit: "Y caused X" (__cause__)raise X (during handling) - Implicit: "X happened while handling Y" (__context__)raise X from None - Suppress chain (hide internal errors)Use explicit chaining when converting internal errors to public API errors.
Common Mistakes
1. Raising wrong exception type
# Wrong - RuntimeError for validation
def set_age(age):
if age < 0:
raise RuntimeError("Age cannot be negative") # Wrong type
# Correct - ValueError for invalid values
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative") # Right type
# Wrong - using Exception (too generic)
raise Exception("Something went wrong")
# Correct - specific exception
raise ConnectionError("Failed to connect to database")
2. Losing traceback when re-raising
# Wrong - creates new exception, loses traceback!
try:
process_data()
except ValueError as e:
log_error(e)
raise ValueError(str(e)) # NEW exception, lost traceback!
# Correct - bare raise preserves traceback
try:
process_data()
except ValueError:
log_error("Processing failed")
raise # Same exception, same traceback
# Also correct - explicit chaining preserves cause
try:
process_data()
except ValueError as e:
raise ProcessingError("Failed to process") from e
3. Forgetting to chain exceptions
# Wrong - original error lost
def get_config():
try:
return json.load(open("config.json"))
except (FileNotFoundError, json.JSONDecodeError):
raise ConfigError("Bad config") # What was the original error?
# Correct - chain with 'from'
def get_config():
try:
return json.load(open("config.json"))
except (FileNotFoundError, json.JSONDecodeError) as e:
raise ConfigError("Bad config") from e # Original preserved!
4. Inheriting from BaseException
# Wrong - BaseException isn't caught by 'except Exception'
class MyError(BaseException): # Wrong!
pass
try:
raise MyError("oops")
except Exception:
print("Not caught!") # MyError escapes!
# Correct - inherit from Exception
class MyError(Exception): # Correct!
pass
try:
raise MyError("oops")
except Exception:
print("Caught!") # Works!
5. Not calling super().__init__ in custom exception
# Wrong - forgets super().__init__
class APIError(Exception):
def __init__(self, code, message):
self.code = code
self.message = message
# Forgot super().__init__!
err = APIError(404, "Not found")
print(str(err)) # Empty string!
# Correct - call super().__init__
class APIError(Exception):
def __init__(self, code, message):
super().__init__(f"[{code}] {message}")
self.code = code
self.message = message
err = APIError(404, "Not found")
print(str(err)) # "[404] Not found"
Exercise: Validation Library
Task: Create a validation library with custom exceptions.
Requirements:
- Create a base
ValidationErrorexception - Create specific exceptions:
RequiredFieldError,TypeValidationError,RangeError - Write a
validate_user()function that validates user data - Chain exceptions properly to preserve context
Click Run to execute your code
Show Solution
# Custom exceptions
class ValidationError(Exception):
"""Base validation error."""
pass
class RequiredFieldError(ValidationError):
"""Field is required but missing."""
def __init__(self, field):
super().__init__(f"Field '{field}' is required")
self.field = field
class TypeValidationError(ValidationError):
"""Field has wrong type."""
def __init__(self, field, expected, got):
super().__init__(f"Field '{field}' expected {expected}, got {got}")
self.field = field
self.expected = expected
self.got = got
class RangeError(ValidationError):
"""Value out of allowed range."""
def __init__(self, field, value, min_val=None, max_val=None):
if min_val is not None and max_val is not None:
msg = f"Field '{field}' value {value} not in range [{min_val}, {max_val}]"
elif min_val is not None:
msg = f"Field '{field}' value {value} must be >= {min_val}"
else:
msg = f"Field '{field}' value {value} must be <= {max_val}"
super().__init__(msg)
self.field = field
self.value = value
def validate_user(data):
"""Validate user data dictionary."""
# Check required fields
for field in ['username', 'email', 'age']:
if field not in data or data[field] is None:
raise RequiredFieldError(field)
# Validate types
if not isinstance(data['username'], str):
raise TypeValidationError('username', 'str', type(data['username']).__name__)
if not isinstance(data['email'], str):
raise TypeValidationError('email', 'str', type(data['email']).__name__)
if not isinstance(data['age'], int):
raise TypeValidationError('age', 'int', type(data['age']).__name__)
# Validate ranges
if len(data['username']) < 3:
raise RangeError('username length', len(data['username']), min_val=3)
if data['age'] < 0 or data['age'] > 150:
raise RangeError('age', data['age'], min_val=0, max_val=150)
if '@' not in data['email']:
raise ValidationError("Email must contain '@'")
return True
# Test the validation
test_users = [
{},
{'username': 'ab', 'email': '[email protected]', 'age': 25},
{'username': 'alice', 'email': 'noatsign', 'age': 25},
{'username': 'bob', 'email': '[email protected]', 'age': -5},
{'username': 'charlie', 'email': '[email protected]', 'age': 30},
]
print("=== User Validation Tests ===\n")
for user in test_users:
try:
validate_user(user)
print(f"Valid: {user}")
except ValidationError as e:
print(f"Invalid: {type(e).__name__}: {e}")
Summary
- raise:
raise ValueError("message")throws an exception - Choose right type: ValueError for bad values, TypeError for wrong types
- Bare raise:
raisere-raises current exception with traceback - Custom exceptions: Inherit from
Exception, callsuper().__init__() - Exception hierarchy: Create base exception, specific ones inherit from it
- raise from:
raise X from Ychains exceptions (Y caused X) - from None:
raise X from Nonesuppresses chain - Add attributes: Custom exceptions can have extra data (code, field, etc.)
What's Next?
Now you can both catch and raise exceptions. But sometimes you just want to check
that something is true during development - not handle it as an error, but catch bugs
early. Next, we'll learn about assert statements for debugging and
development-time checks!
Enjoying these tutorials?