Raising Exceptions
Learning Objectives
- By the end of this lesson, you will be able to:
- - Use the `raise` statement to raise exceptions
- - Create custom exception classes
- - Understand exception chaining
- - Raise exceptions with custom messages
- - Re-raise exceptions
- - Use exception chaining with `from`
- - Design exception hierarchies
- - Apply custom exceptions in practical scenarios
- - Understand when to raise exceptions vs return error values
Lesson 10.3: Raising Exceptions
Learning Objectives
By the end of this lesson, you will be able to:
- Use the
raisestatement to raise exceptions - Create custom exception classes
- Understand exception chaining
- Raise exceptions with custom messages
- Re-raise exceptions
- Use exception chaining with
from - Design exception hierarchies
- Apply custom exceptions in practical scenarios
- Understand when to raise exceptions vs return error values
Introduction to Raising Exceptions
Raising exceptions allows you to signal that an error or exceptional condition has occurred. You can raise built-in exceptions or create custom ones.
Why Raise Exceptions?
- Signal errors: Indicate that something went wrong
- Validation: Enforce constraints and rules
- Control flow: Stop execution when conditions aren't met
- API design: Provide clear error messages to users
The raise Statement
Basic raise Syntax
raise ExceptionType("Error message")
Raising Built-in Exceptions
# Raise ValueError
if age < 0:
raise ValueError("Age cannot be negative")
# Raise TypeError
if not isinstance(name, str):
raise TypeError("Name must be a string")
# Raise custom message
if divisor == 0:
raise ZeroDivisionError("Cannot divide by zero")
Basic Examples
# Raise ValueError
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
return age
# Usage
try:
set_age(-5)
except ValueError as e:
print(f"Error: {e}")
Raising Without Arguments
# Re-raise current exception
try:
risky_operation()
except Exception:
# Do some handling
raise # Re-raise the same exception
Raising Exceptions with Messages
Custom Error Messages
def divide(a, b):
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError("Both arguments must be numbers")
return a / b
# Usage
try:
result = divide(10, 0)
except ZeroDivisionError as e:
print(f"Error: {e}")
Formatting Error Messages
def validate_age(age):
if not isinstance(age, int):
raise TypeError(f"Expected int, got {type(age).__name__}")
if age < 0:
raise ValueError(f"Age {age} cannot be negative")
if age > 150:
raise ValueError(f"Age {age} seems unrealistic")
return age
Example: Input Validation
def get_positive_number(value):
try:
number = float(value)
except ValueError:
raise ValueError(f"'{value}' is not a valid number")
if number <= 0:
raise ValueError(f"Number must be positive, got {number}")
return number
# Usage
try:
num = get_positive_number("-5")
except ValueError as e:
print(f"Validation error: {e}")
Creating Custom Exceptions
Basic Custom Exception
class CustomError(Exception):
pass
# Use it
raise CustomError("Something went wrong")
Custom Exception with Message
class ValidationError(Exception):
"""Exception raised for validation errors."""
pass
# Use it
if not valid:
raise ValidationError("Validation failed")
Custom Exception with Attributes
class AgeError(Exception):
"""Exception raised for age-related errors."""
def __init__(self, age, message="Invalid age"):
self.age = age
self.message = message
super().__init__(self.message)
def __str__(self):
return f"{self.message}: {age}"
# Use it
if age < 0:
raise AgeError(age, "Age cannot be negative")
Example: Custom Validation Exception
class InvalidInputError(Exception):
"""Exception for invalid input."""
def __init__(self, input_value, reason):
self.input_value = input_value
self.reason = reason
message = f"Invalid input '{input_value}': {reason}"
super().__init__(message)
# Use it
def validate_email(email):
if "@" not in email:
raise InvalidInputError(email, "Missing @ symbol")
if "." not in email:
raise InvalidInputError(email, "Missing domain")
return email
try:
validate_email("invalid")
except InvalidInputError as e:
print(f"Error: {e}")
print(f"Input: {e.input_value}, Reason: {e.reason}")
Exception Hierarchies
Creating Exception Hierarchy
class DatabaseError(Exception):
"""Base exception for database errors."""
pass
class ConnectionError(DatabaseError):
"""Exception for connection errors."""
pass
class QueryError(DatabaseError):
"""Exception for query errors."""
pass
class TimeoutError(ConnectionError):
"""Exception for timeout errors."""
pass
# Use hierarchy
try:
# Database operation
pass
except TimeoutError:
print("Connection timeout")
except ConnectionError:
print("Connection error")
except DatabaseError:
print("Database error")
Example: Application Exception Hierarchy
class ApplicationError(Exception):
"""Base exception for application."""
pass
class ValidationError(ApplicationError):
"""Exception for validation errors."""
pass
class BusinessLogicError(ApplicationError):
"""Exception for business logic errors."""
pass
class DataError(ApplicationError):
"""Exception for data errors."""
pass
class NotFoundError(DataError):
"""Exception when resource not found."""
pass
class DuplicateError(DataError):
"""Exception when duplicate found."""
pass
# Use it
def create_user(username):
if not username:
raise ValidationError("Username cannot be empty")
if user_exists(username):
raise DuplicateError(f"User '{username}' already exists")
# Create user
Exception Chaining
Basic Exception Chaining
Exception chaining preserves the original exception when raising a new one.
Using from
try:
result = 10 / 0
except ZeroDivisionError as e:
raise ValueError("Invalid operation") from e
Example: Exception Chaining
def process_file(filename):
try:
with open(filename, "r") as file:
content = file.read()
number = int(content)
return number
except FileNotFoundError as e:
raise ValueError(f"Cannot process file: {filename}") from e
except ValueError as e:
raise ValueError(f"Invalid content in file: {filename}") from e
# Usage
try:
result = process_file("data.txt")
except ValueError as e:
print(f"Error: {e}")
print(f"Original: {e.__cause__}")
Chaining Without from
# Without 'from'
try:
result = 10 / 0
except ZeroDivisionError:
raise ValueError("Invalid operation")
# Original exception is lost
Suppressing Exception Chaining
try:
result = 10 / 0
except ZeroDivisionError:
raise ValueError("Invalid operation") from None
# Suppresses the original exception
Re-raising Exceptions
Re-raise Current Exception
try:
risky_operation()
except Exception:
# Do some logging or cleanup
print("Error occurred, logging...")
raise # Re-raise the same exception
Example: Re-raising
def process_data(data):
try:
result = complex_operation(data)
return result
except ValueError:
# Log the error
print("Value error in complex_operation")
raise # Re-raise to caller
except TypeError:
# Handle differently
print("Type error in complex_operation")
raise
# Usage
try:
result = process_data(invalid_data)
except ValueError:
print("Handled at caller level")
Practical Examples
Example 1: Validation with Custom Exception
class ValidationError(Exception):
"""Custom validation exception."""
pass
def validate_user(username, age, email):
errors = []
if not username or len(username) < 3:
errors.append("Username must be at least 3 characters")
if not isinstance(age, int) or age < 0:
errors.append("Age must be a non-negative integer")
if "@" not in email:
errors.append("Invalid email format")
if errors:
raise ValidationError("; ".join(errors))
return True
# Usage
try:
validate_user("ab", -5, "invalid-email")
except ValidationError as e:
print(f"Validation failed: {e}")
Example 2: API Error Handling
class APIError(Exception):
"""Base exception for API errors."""
pass
class AuthenticationError(APIError):
"""Exception for authentication errors."""
pass
class RateLimitError(APIError):
"""Exception for rate limit errors."""
pass
class ServerError(APIError):
"""Exception for server errors."""
pass
def make_api_request(url):
# Simulate API call
if "auth" in url:
raise AuthenticationError("Invalid credentials")
elif "rate" in url:
raise RateLimitError("Rate limit exceeded")
elif "server" in url:
raise ServerError("Server error")
return {"status": "success"}
# Usage
try:
response = make_api_request("api/auth")
except AuthenticationError:
print("Please check your credentials")
except RateLimitError:
print("Please wait before trying again")
except APIError as e:
print(f"API error: {e}")
Example 3: File Processing with Chaining
class FileProcessingError(Exception):
"""Exception for file processing errors."""
pass
def process_config_file(filename):
try:
with open(filename, "r") as file:
config = json.load(file)
return config
except FileNotFoundError as e:
raise FileProcessingError(f"Config file not found: {filename}") from e
except json.JSONDecodeError as e:
raise FileProcessingError(f"Invalid JSON in {filename}") from e
except Exception as e:
raise FileProcessingError(f"Error processing {filename}") from e
# Usage
try:
config = process_config_file("config.json")
except FileProcessingError as e:
print(f"Error: {e}")
if e.__cause__:
print(f"Original error: {e.__cause__}")
Example 4: Business Logic Exceptions
class InsufficientFundsError(Exception):
"""Exception for insufficient funds."""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
message = f"Insufficient funds: balance={balance}, requested={amount}"
super().__init__(message)
class Account:
def __init__(self, balance=0):
self.balance = balance
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
return self.balance
# Usage
account = Account(100)
try:
account.withdraw(150)
except InsufficientFundsError as e:
print(f"Error: {e}")
print(f"Balance: ${e.balance}, Requested: ${e.amount}")
Best Practices
1. Use Specific Exceptions
# Good: Specific exception
if age < 0:
raise ValueError("Age cannot be negative")
# Avoid: Generic exception
if age < 0:
raise Exception("Age cannot be negative")
2. Provide Clear Messages
# Good: Clear message
raise ValueError(f"Age {age} is invalid. Must be between 0 and 150.")
# Avoid: Vague message
raise ValueError("Invalid")
3. Create Custom Exceptions for Your Domain
# Good: Domain-specific exception
class UserNotFoundError(Exception):
pass
# Avoid: Using generic exceptions
raise ValueError("User not found") # Less clear
4. Use Exception Hierarchies
# Good: Hierarchical exceptions
class DatabaseError(Exception):
pass
class ConnectionError(DatabaseError):
pass
# Allows catching at different levels
5. Preserve Exception Context
# Good: Preserve original exception
try:
operation()
except OriginalError as e:
raise NewError("Context") from e
Common Mistakes and Pitfalls
1. Raising Generic Exceptions
# WRONG: Too generic
raise Exception("Error occurred")
# BETTER: Specific exception
raise ValueError("Invalid value")
2. Losing Exception Context
# WRONG: Loses original exception
try:
operation()
except OriginalError:
raise NewError("Error") # Original lost
# BETTER: Preserve context
try:
operation()
except OriginalError as e:
raise NewError("Error") from e
3. Not Providing Error Messages
# WRONG: No message
raise ValueError()
# BETTER: Clear message
raise ValueError("Age cannot be negative")
4. Raising Instead of Returning
# WRONG: Using exception for control flow
def find_user(username):
if not user_exists(username):
raise NotFoundError()
return user
# BETTER: Return None or use exception appropriately
def find_user(username):
if not user_exists(username):
return None # Or raise if it's truly exceptional
return user
When to Raise Exceptions
✅ Good Use Cases
- Validation errors: Invalid input
- Resource errors: File not found, network error
- Business logic errors: Insufficient funds, invalid state
- Programming errors: Wrong type, missing required parameter
❌ Avoid For
- Normal control flow: Use if/else instead
- Expected conditions: Return None or use optional values
- Performance: Exceptions have overhead
Practice Exercise
Exercise: Custom Exceptions
Objective: Create a Python program that demonstrates raising exceptions and creating custom exceptions.
Instructions:
-
Create a file called
custom_exceptions_practice.py -
Write a program that:
- Raises built-in exceptions
- Creates custom exception classes
- Uses exception chaining
- Implements exception hierarchies
- Applies exceptions in practical scenarios
-
Your program should include:
- Raising exceptions with messages
- Custom exception classes
- Exception hierarchies
- Exception chaining
- Practical exception usage
Example Solution:
"""
Custom Exceptions Practice
This program demonstrates raising exceptions and creating custom exceptions.
"""
print("=" * 60)
print("CUSTOM EXCEPTIONS PRACTICE")
print("=" * 60)
print()
# 1. Raising built-in exceptions
print("1. RAISING BUILT-IN EXCEPTIONS")
print("-" * 60)
def validate_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age seems unrealistic")
return age
try:
validate_age(-5)
except ValueError as e:
print(f"Caught: {e}")
print()
# 2. Basic custom exception
print("2. BASIC CUSTOM EXCEPTION")
print("-" * 60)
class CustomError(Exception):
pass
try:
raise CustomError("Something went wrong")
except CustomError as e:
print(f"Caught custom error: {e}")
print()
# 3. Custom exception with message
print("3. CUSTOM EXCEPTION WITH MESSAGE")
print("-" * 60)
class ValidationError(Exception):
"""Exception for validation errors."""
pass
def validate_email(email):
if "@" not in email:
raise ValidationError(f"Invalid email: '{email}' missing @")
return email
try:
validate_email("invalid-email")
except ValidationError as e:
print(f"Validation error: {e}")
print()
# 4. Custom exception with attributes
print("4. CUSTOM EXCEPTION WITH ATTRIBUTES")
print("-" * 60)
class AgeError(Exception):
"""Exception for age-related errors."""
def __init__(self, age, message="Invalid age"):
self.age = age
self.message = message
super().__init__(f"{message}: {age}")
try:
age = -5
if age < 0:
raise AgeError(age, "Age cannot be negative")
except AgeError as e:
print(f"Error: {e}")
print(f"Age value: {e.age}")
print()
# 5. Exception hierarchy
print("5. EXCEPTION HIERARCHY")
print("-" * 60)
class DatabaseError(Exception):
"""Base exception for database errors."""
pass
class ConnectionError(DatabaseError):
"""Exception for connection errors."""
pass
class QueryError(DatabaseError):
"""Exception for query errors."""
pass
try:
raise ConnectionError("Cannot connect to database")
except ConnectionError as e:
print(f"Connection error: {e}")
except DatabaseError as e:
print(f"Database error: {e}")
print()
# 6. Exception chaining with from
print("6. EXCEPTION CHAINING WITH FROM")
print("-" * 60)
def process_file(filename):
try:
with open(filename, "r") as file:
content = file.read()
number = int(content)
return number
except FileNotFoundError as e:
raise ValueError(f"Cannot process file: {filename}") from e
try:
result = process_file("nonexistent.txt")
except ValueError as e:
print(f"Error: {e}")
print(f"Original error: {e.__cause__}")
print()
# 7. Re-raising exceptions
print("7. RE-RAISING EXCEPTIONS")
print("-" * 60)
def process_data(data):
try:
if not isinstance(data, int):
raise TypeError("Data must be an integer")
return data * 2
except TypeError:
print("Type error in process_data, re-raising...")
raise
try:
result = process_data("not a number")
except TypeError as e:
print(f"Caught at caller: {e}")
print()
# 8. Application exception hierarchy
print("8. APPLICATION EXCEPTION HIERARCHY")
print("-" * 60)
class ApplicationError(Exception):
"""Base exception for application."""
pass
class ValidationError(ApplicationError):
"""Exception for validation errors."""
pass
class BusinessLogicError(ApplicationError):
"""Exception for business logic errors."""
pass
class DataError(ApplicationError):
"""Exception for data errors."""
pass
class NotFoundError(DataError):
"""Exception when resource not found."""
pass
def find_user(username):
if not username:
raise ValidationError("Username cannot be empty")
# Simulate not found
raise NotFoundError(f"User '{username}' not found")
try:
find_user("")
except ValidationError as e:
print(f"Validation error: {e}")
try:
find_user("alice")
except NotFoundError as e:
print(f"Not found: {e}")
except DataError as e:
print(f"Data error: {e}")
print()
# 9. Custom exception with multiple attributes
print("9. CUSTOM EXCEPTION WITH MULTIPLE ATTRIBUTES")
print("-" * 60)
class InvalidInputError(Exception):
"""Exception for invalid input."""
def __init__(self, input_value, reason, field=None):
self.input_value = input_value
self.reason = reason
self.field = field
message = f"Invalid input '{input_value}': {reason}"
if field:
message += f" (field: {field})"
super().__init__(message)
def validate_form(name, age, email):
errors = []
if not name:
errors.append(InvalidInputError(name, "Cannot be empty", "name"))
if not isinstance(age, int) or age < 0:
errors.append(InvalidInputError(age, "Must be non-negative integer", "age"))
if "@" not in email:
errors.append(InvalidInputError(email, "Invalid format", "email"))
if errors:
raise ExceptionGroup("Validation errors", errors)
return True
try:
validate_form("", -5, "invalid")
except* InvalidInputError as e:
for error in e.exceptions:
print(f"Error in {error.field}: {error.reason}")
except ExceptionGroup as e:
print(f"Multiple errors: {len(e.exceptions)}")
print()
# 10. Business logic exception
print("10. BUSINESS LOGIC EXCEPTION")
print("-" * 60)
class InsufficientFundsError(Exception):
"""Exception for insufficient funds."""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
message = f"Insufficient funds: balance=${balance}, requested=${amount}"
super().__init__(message)
class Account:
def __init__(self, balance=0):
self.balance = balance
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
return self.balance
account = Account(100)
try:
account.withdraw(150)
except InsufficientFundsError as e:
print(f"Error: {e}")
print(f"Balance: ${e.balance}, Requested: ${e.amount}")
print()
# 11. API error exceptions
print("11. API ERROR EXCEPTIONS")
print("-" * 60)
class APIError(Exception):
"""Base exception for API errors."""
pass
class AuthenticationError(APIError):
"""Exception for authentication errors."""
pass
class RateLimitError(APIError):
"""Exception for rate limit errors."""
pass
class ServerError(APIError):
"""Exception for server errors."""
pass
def make_request(endpoint):
if "auth" in endpoint:
raise AuthenticationError("Invalid credentials")
elif "rate" in endpoint:
raise RateLimitError("Rate limit exceeded")
return {"status": "success"}
try:
response = make_request("api/auth")
except AuthenticationError:
print("Please check your credentials")
except APIError as e:
print(f"API error: {e}")
print()
# 12. Exception with context
print("12. EXCEPTION WITH CONTEXT")
print("-" * 60)
class ProcessingError(Exception):
"""Exception with processing context."""
def __init__(self, message, context=None):
self.context = context
if context:
message = f"{message} (context: {context})"
super().__init__(message)
def process_item(item, context):
if not item:
raise ProcessingError("Item is empty", context)
return item.upper()
try:
result = process_item("", "user_input")
except ProcessingError as e:
print(f"Error: {e}")
print(f"Context: {e.context}")
print()
# 13. Suppressing exception chaining
print("13. SUPPRESSING EXCEPTION CHAINING")
print("-" * 60)
try:
result = 10 / 0
except ZeroDivisionError:
raise ValueError("Invalid operation") from None
# Original exception is suppressed
except ValueError as e:
print(f"Error: {e}")
print(f"Original suppressed: {e.__cause__ is None}")
print()
# 14. Multiple exception types
print("14. MULTIPLE EXCEPTION TYPES")
print("-" * 60)
class ValidationError(Exception):
pass
class FormatError(ValidationError):
pass
class RangeError(ValidationError):
pass
def validate_number(value, min_val=0, max_val=100):
try:
number = float(value)
except ValueError:
raise FormatError(f"'{value}' is not a number")
if number < min_val:
raise RangeError(f"Value {number} is below minimum {min_val}")
if number > max_val:
raise RangeError(f"Value {number} is above maximum {max_val}")
return number
try:
validate_number("abc")
except FormatError as e:
print(f"Format error: {e}")
try:
validate_number("150")
except RangeError as e:
print(f"Range error: {e}")
print()
# 15. Exception with default message
print("15. EXCEPTION WITH DEFAULT MESSAGE")
print("-" * 60)
class ConfigurationError(Exception):
"""Exception for configuration errors."""
def __init__(self, message="Configuration error occurred"):
self.message = message
super().__init__(self.message)
try:
raise ConfigurationError()
except ConfigurationError as e:
print(f"Default message: {e}")
try:
raise ConfigurationError("Custom configuration error")
except ConfigurationError as e:
print(f"Custom message: {e}")
print()
print("=" * 60)
print("PRACTICE COMPLETE!")
print("=" * 60)
Expected Output (truncated):
============================================================
CUSTOM EXCEPTIONS PRACTICE
============================================================
1. RAISING BUILT-IN EXCEPTIONS
------------------------------------------------------------
Caught: Age cannot be negative
[... rest of output ...]
Challenge (Optional):
- Create a complete exception hierarchy for your application
- Build a validation framework with custom exceptions
- Implement error recovery mechanisms
- Create exception logging system
Key Takeaways
raisestatement raises exceptions- Raise built-in exceptions for standard errors
- Create custom exceptions for domain-specific errors
- Exception hierarchies allow catching at different levels
- Exception chaining preserves original exception with
from - Re-raise exceptions with
raise(no arguments) - Provide clear error messages in exceptions
- Use specific exceptions rather than generic ones
- Custom exceptions should inherit from
Exception - Exception attributes can store additional information
- Exception hierarchies help organize related errors
- Preserve exception context when chaining
- Use exceptions for exceptional cases, not normal flow
- Document custom exceptions with docstrings
- Exception chaining helps debug complex error scenarios
Quiz: Raising Exceptions
Test your understanding with these questions:
-
What statement raises an exception?
- A)
throw - B)
raise - C)
except - D)
error
- A)
-
How do you create a custom exception?
- A)
class MyError(Exception) - B)
def MyError(Exception) - C)
MyError = Exception() - D)
raise MyError()
- A)
-
What does
raisewithout arguments do?- A) Raises new exception
- B) Re-raises current exception
- C) Does nothing
- D) Suppresses exception
-
How do you chain exceptions?
- A)
raise NewError from old_error - B)
raise NewError(old_error) - C)
raise NewError, old_error - D)
raise NewError with old_error
- A)
-
What should custom exceptions inherit from?
- A)
BaseException - B)
Exception - C)
Error - D)
object
- A)
-
What does
from Nonedo in exception chaining?- A) Preserves original
- B) Suppresses original
- C) Creates new exception
- D) Does nothing
-
When should you raise exceptions?
- A) For normal control flow
- B) For exceptional conditions
- C) Always
- D) Never
-
What is the benefit of exception hierarchies?
- A) Faster execution
- B) Can catch at different levels
- C) Smaller code
- D) No benefit
-
How do you preserve exception context?
- A)
raise NewError() - B)
raise NewError() from e - C)
raise NewError(), e - D)
raise NewError(e)
- A)
-
What should exception messages be?
- A) Vague
- B) Clear and descriptive
- C) Empty
- D) Technical only
Answers:
- B)
raise(statement to raise exceptions) - A)
class MyError(Exception)(inherit from Exception) - B) Re-raises current exception (raise without args re-raises)
- A)
raise NewError from old_error(exception chaining syntax) - B)
Exception(custom exceptions should inherit from Exception) - B) Suppresses original (from None suppresses chaining)
- B) For exceptional conditions (not normal flow)
- B) Can catch at different levels (hierarchy benefit)
- B)
raise NewError() from e(preserves context) - B) Clear and descriptive (good error messages)
Next Steps
Excellent work! You've mastered raising exceptions. You now understand:
- How to raise exceptions
- How to create custom exceptions
- How to use exception chaining
- How to design exception hierarchies
What's Next?
- Lesson 10.4: Debugging Techniques
- Learn debugging methods
- Use debuggers
- Understand debugging tools
Additional Resources
- Raising Exceptions: docs.python.org/3/tutorial/errors.html#raising-exceptions
- Exception Classes: docs.python.org/3/tutorial/errors.html#user-defined-exceptions
- Exception Chaining: docs.python.org/3/tutorial/errors.html#exception-chaining
Lesson completed! You're ready to move on to the next lesson.
Course Navigation
- Exceptions
- Try-Except Blocks
- Raising Exceptions
- Debugging Techniques