Previous | Tutorial index | Next
raiseraise statement in a program.So far, we have seen how Python automatically raises exceptions when something goes wrong (e.g., dividing by zero, accessing a missing key). However, in many situations, you need to explicitly signal that an error has occurred in your code, even when Python itself doesn't see a problem. This is where the raise statement comes into play.
The raise statement allows you to purposefully throw an exception at any point in your program. This is essential for:
ValueError for invalid data).RuntimeError when a withdrawal exceeds a balance).NotImplementedError).By raising exceptions, you make your code more robust, maintainable, and easier to debug. You are explicitly stating: "This condition is an error, and it should be handled by whoever is calling this code."
Before diving into the content, familiarise yourself with these key terms:
| Term | Definition |
|---|---|
raise |
Statement to purposely throw an exception. |
| Re‑raising | Using raise without arguments inside an except block to re‑throw the current exception. |
| Exception chaining | Using raise ... from to link a new exception to the original cause. |
raise ... from None |
Suppresses the original exception context. |
NotImplementedError |
Common built‑in used to indicate a method must be overridden. |
Imagine you are a quality control inspector at a factory. You receive a product that looks OK on the outside, but you check its specifications and find that its weight is below the required minimum. Python wouldn't automatically "see" this because it doesn't know the business rule. But you, as the inspector, know the rule. So you raise an alarm (throw an exception) to stop the production line and notify the supervisors. This is exactly what raise does: it allows you to signal a problem that the interpreter wouldn't detect on its own.
raise Statement: Syntax and Basic UsageThe raise statement can be used in several ways:
raise ExceptionClass # Raises an instance of ExceptionClass
raise ExceptionClass() # Raises an instance (same as above)
raise ExceptionClass("message") # Raises with an error message
raise # Re-raises the current exception (only inside except block)
raise ExceptionClass from cause # Exception chaining
You can raise any built-in exception class. The most common ones are ValueError, TypeError, RuntimeError, NotImplementedError, and AssertionError (though assert is often used for that).
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative.")
if age > 150:
raise ValueError("Age cannot exceed 150.")
print(f"Age set to {age}")
Key Point: You are raising an instance of the exception class. The error message is optional but highly recommended.
You can raise an exception instance directly:
error = ValueError("Invalid input")
raise error
This is equivalent to raise ValueError("Invalid input").
Always include a clear, descriptive message that explains what went wrong. This helps the caller (and the user) understand the issue.
def withdraw(amount, balance):
if amount > balance:
raise ValueError(f"Insufficient balance: requested {amount}, available {balance}")
return balance - amount
When a function receives arguments that are invalid (wrong type, out of range, etc.), it should raise an exception rather than silently accepting bad data.
def calculate_square_root(x):
if not isinstance(x, (int, float)):
raise TypeError("Argument must be a number")
if x < 0:
raise ValueError("Cannot compute square root of negative number")
return x ** 0.5
Business logic often has constraints that are not enforced by the type system. For example, an e-commerce system might have a rule that a discount cannot exceed 50%.
def apply_discount(price, discount_percent):
if discount_percent < 0 or discount_percent > 50:
raise ValueError("Discount must be between 0% and 50%")
return price * (1 - discount_percent / 100)
When designing abstract base classes or interfaces, you can raise NotImplementedError to indicate that a method must be overridden by subclasses.
class Animal:
def speak(self):
raise NotImplementedError("Subclasses must implement speak()")
Defensive programming often uses raise to check that inputs meet expectations and outputs are valid.
def divide(a, b):
if b == 0:
raise ZeroDivisionError("Denominator cannot be zero")
result = a / b
# Postcondition: result should be finite
if not math.isfinite(result):
raise RuntimeError("Result is not finite")
return result
Sometimes you catch an exception, log it, and then re-raise it to allow higher-level handlers to deal with it.
raise Without ArgumentsWhen you catch an exception but cannot fully handle it, you may want to re-raise the same exception after performing some actions (e.g., logging, cleanup). Using raise without arguments inside an except block re-raises the current exception exactly as it was.
try:
risky_operation()
except ValueError as e:
print(f"Logging: {e}")
raise # Re-raise the ValueError
import logging
def process_data(data):
try:
result = int(data) * 10
except ValueError:
logging.error(f"Invalid data: {data}")
raise # Re-raise for the caller to handle
return result
try:
process_data("abc")
except ValueError:
print("Caller: Caught the re-raised ValueError.")
Key Points:
raise and raise etry:
1 / 0
except ZeroDivisionError as e:
raise e # This raises a new instance; the traceback may be truncated
raise # This re-raises the original exception with full traceback
Prefer raise without arguments to preserve the original stack trace.
raise ... fromSometimes, when you catch one exception, you want to raise a different, more appropriate exception for the caller. Exception chaining allows you to link the new exception to the original cause using the from keyword.
try:
risky_operation()
except ValueError as e:
raise RuntimeError("Operation failed") from e
This creates a chain: the RuntimeError is raised, and its __cause__ attribute points to the original ValueError. The traceback will show both exceptions.
def read_user_age(filename):
try:
with open(filename, 'r') as f:
age_str = f.read().strip()
return int(age_str)
except FileNotFoundError as e:
raise RuntimeError("User data file missing") from e
except ValueError as e:
raise RuntimeError("User age is not a valid integer") from e
try:
age = read_user_age("age.txt")
except RuntimeError as e:
print(e) # "User age is not a valid integer"
print(e.__cause__) # The original ValueError
from NoneIf you want to suppress the original exception context and not show it in the traceback, use from None.
try:
int("abc")
except ValueError:
raise RuntimeError("Failed to convert") from None
The traceback will only show the RuntimeError without mentioning the ValueError.
When to use: If the original cause is not relevant to the caller or if it might confuse them.
Always include a message that explains what went wrong and, if possible, what the correct value should be.
def set_password(password):
if len(password) < 8:
raise ValueError("Password must be at least 8 characters long")
# ...
You can raise TypeError when a function is called with an argument of the wrong type.
def double_number(x):
if not isinstance(x, (int, float)):
raise TypeError("Expected a number")
return x * 2
Encapsulate validation logic in separate functions that raise exceptions.
def validate_age(age):
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0 or age > 150:
raise ValueError("Age must be between 0 and 150")
return age
# Usage
try:
age = validate_age(int(input("Enter age: ")))
except (TypeError, ValueError) as e:
print(f"Invalid input: {e}")
Do not raise Exception directly; use a more specific built-in exception or create a custom one (see Tutorial 6). Raising Exception makes it difficult for callers to handle it specifically.
# Bad:
raise Exception("Something went wrong")
# Good:
raise ValueError("Invalid data")
In function docstrings, list the exceptions that the function may raise (using :raises: in Sphinx/Google style). This helps users of your function.
def divide(a, b):
"""
Divide a by b.
:param a: numerator
:param b: denominator
:return: division result
:raises ZeroDivisionError: if b is zero
"""
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
If you catch an exception and want to add more context but keep the original type, you can raise a new exception of the same type with a more informative message.
try:
int(user_input)
except ValueError as e:
raise ValueError(f"Invalid number format: {user_input}") from e
raise vs assert| Feature | raise |
assert |
|---|---|---|
| Purpose | Signal an error condition at runtime | Test assumptions during development |
| Can be disabled | No | Yes (with -O flag) |
| Used for | Input validation, business rules, preconditions | Debugging, internal invariants |
| Example | raise ValueError("Invalid input") |
assert x > 0, "x must be positive" |
| Best practice | Always use in production code for validation | Use for debugging; not for production validation |
Rule of thumb: Use raise for conditions that should always be checked. Use assert for conditions that should only be checked during development to catch bugs.
raiseimport math
def get_positive_integer(prompt):
"""
Prompt the user for a positive integer.
Raises ValueError if input is not a positive integer.
"""
try:
value = int(input(prompt))
except ValueError as e:
raise ValueError("Input must be a valid integer") from e
if value <= 0:
raise ValueError("Input must be a positive integer")
return value
def calculate_factorial(n):
if n < 0:
raise ValueError("Factorial not defined for negative numbers")
return math.factorial(n)
def main():
try:
n = get_positive_integer("Enter a positive integer: ")
fact = calculate_factorial(n)
print(f"{n}! = {fact}")
except ValueError as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
| Feature | Description |
|---|---|
raise |
Explicitly signal an error condition. |
| Re‑raising | raise inside except re‑throws current exception. |
| Exception chaining | raise NewError from original preserves the cause. |
| Suppressing cause | raise NewError from None hides the original. |
| Best practice | Use specific built‑ins or custom exceptions, provide clear messages, and document them. |
| When to use | Input validation, business rules, and any condition that must always be enforced. |
Test your understanding of the concepts covered in this tutorial. Answer each question, then click to reveal the correct answer.
Q1: Which statement is used to purposely throw an exception in Python?
Q2: What is the output of the following code?
try:
raise ValueError("Oops")
except ValueError:
print("Caught")
raise
Q3: True or False: You can only raise built‑in exceptions; you cannot raise your own classes.
Q4: What is the purpose of raise ... from?
Q5: Which of the following is a good use of raise?
Q6: What happens if you use raise without any arguments outside of an except block?
RuntimeError.TypeError.ValueError.SystemError.Q7: In exception chaining, which attribute of the raised exception refers to the original cause?
__context____cause____traceback____source__Q8: Which is better for enforcing input validation in production code?
assertraiseprint.Q9: Given the following code, what will be printed?
try:
try:
raise ValueError("Inner")
except ValueError as e:
raise RuntimeError("Outer") from e
except RuntimeError as e:
print(e.__cause__)
OuterInnerValueError: InnerNoneQ10: Which of the following correctly re‑raises the current exception while preserving the traceback?
raiseraise eraise Exception()raise ValueErrorComplete the exercises below to reinforce your understanding. Sample solutions are provided after each exercise.
Instructions: Write a function create_account(username, password, email) that:
ValueError if username is empty or less than 3 characters.ValueError if password is less than 8 characters.ValueError if email does not contain '@'.Test the function with valid and invalid inputs.
def create_account(username, password, email):
if not username or len(username) < 3:
raise ValueError("Username must be at least 3 characters long")
if len(password) < 8:
raise ValueError("Password must be at least 8 characters long")
if '@' not in email:
raise ValueError("Email must contain '@'")
return {"username": username, "password": password, "email": email}
# Test
try:
user = create_account("al", "pass", "test")
except ValueError as e:
print(e)
Instructions: Write a function safe_open(filename) that:
FileNotFoundError occurs, it prints a log message: "File not found: {filename}" and then re-raises the exception.PermissionError occurs, it prints a log message: "Permission denied: {filename}" and then re-raises the exception.In the main program, call safe_open with a non-existent file, catch the re-raised exception, and print "Caught in main."
def safe_open(filename):
try:
return open(filename, 'r')
except FileNotFoundError:
print(f"File not found: {filename}")
raise
except PermissionError:
print(f"Permission denied: {filename}")
raise
except Exception:
print("Unexpected error")
raise
def main():
try:
file = safe_open("non_existent.txt")
except FileNotFoundError:
print("Caught in main.")
if __name__ == "__main__":
main()
Instructions: Write a function parse_config(filename) that:
json.loads).FileNotFoundError occurs, raise a custom RuntimeError with message "Config file missing" and chain the original exception.json.JSONDecodeError occurs, raise a ValueError with message "Invalid JSON format" and chain the original exception.Test it with a missing file and with a file containing invalid JSON (you can create a temporary file for testing).
import json
def parse_config(filename):
try:
with open(filename, 'r') as f:
content = f.read()
return json.loads(content)
except FileNotFoundError as e:
raise RuntimeError("Config file missing") from e
except json.JSONDecodeError as e:
raise ValueError("Invalid JSON format") from e
# Test (requires creating test files)
raise from NoneInstructions: Write a function connect_to_database(connection_string) that:
ConnectionError occurs (simulate by raising ConnectionError), raise a RuntimeError with message "Database connection failed" but suppress the original cause using from None.RuntimeError in the main program and print the error, ensuring the original ConnectionError is not shown.def connect_to_database(conn_str):
try:
# Simulate a connection error
raise ConnectionError("Cannot connect")
except ConnectionError:
raise RuntimeError("Database connection failed") from None
def main():
try:
connect_to_database("test")
except RuntimeError as e:
print(e)
# e.__cause__ is None
if __name__ == "__main__":
main()
Instructions: Write a function parse_date(date_string) that:
YYYY-MM-DD.ValueError if the format is incorrect.ValueError if the month is not between 1 and 12.ValueError if the day is not valid for the month (you can use datetime for validation).from datetime import datetime
def parse_date(date_string):
"""
Parse a date in YYYY-MM-DD format.
Args:
date_string (str): The date string.
Returns:
datetime.date: The parsed date.
Raises:
ValueError: If format is incorrect, month invalid, or day invalid.
"""
try:
date_obj = datetime.strptime(date_string, "%Y-%m-%d").date()
return date_obj
except ValueError as e:
# Convert to our own ValueError with clearer message
raise ValueError(f"Invalid date format or value: {date_string}") from e
1. You are designing a banking application. Write a function transfer(amount, from_account, to_account) that:
ValueError if amount is not positive.RuntimeError if from_account balance is insufficient.TypeError if any of the arguments are of the wrong type (e.g., amount not a number).Assume accounts are dictionaries with keys 'balance'. Include proper exception handling in the calling code.
def transfer(amount, from_account, to_account):
# Type checks
if not isinstance(amount, (int, float)):
raise TypeError("Amount must be a number")
if not isinstance(from_account, dict) or not isinstance(to_account, dict):
raise TypeError("Accounts must be dictionaries")
if 'balance' not in from_account or 'balance' not in to_account:
raise TypeError("Account dictionaries must have 'balance' key")
if amount <= 0:
raise ValueError("Amount must be positive")
if from_account['balance'] < amount:
raise RuntimeError("Insufficient balance")
from_account['balance'] -= amount
to_account['balance'] += amount
return from_account['balance']
# Usage
try:
transfer(-10, {'balance': 100}, {'balance': 50})
except (ValueError, RuntimeError, TypeError) as e:
print(e)
2. Given the following function that reads a number from a file and squares it:
def square_number_from_file(filename):
with open(filename, 'r') as f:
data = f.read()
num = int(data)
return num * num
Rewrite this function to:
FileNotFoundError and re-raise it with a more informative message: "Could not read number from {filename}".ValueError (from int() conversion) and re-raise it with message: "Invalid number format in {filename}".def square_number_from_file(filename):
try:
with open(filename, 'r') as f:
data = f.read()
except FileNotFoundError as e:
raise RuntimeError(f"Could not read number from {filename}") from e
try:
num = int(data)
except ValueError as e:
raise ValueError(f"Invalid number format in {filename}") from e
return num * num
3. Write a function find_average(numbers) that:
assert) that numbers is a non-empty list (for debugging).TypeError if numbers is not a list.ValueError if any element is not a number.Discuss why you chose assert for the empty list check and raise for the other checks.
def find_average(numbers):
# Debugging assertion (can be disabled)
assert len(numbers) > 0, "List must not be empty"
if not isinstance(numbers, list):
raise TypeError("numbers must be a list")
total = 0
for n in numbers:
if not isinstance(n, (int, float)):
raise ValueError("All elements must be numbers")
total += n
return total / len(numbers)
Discussion: assert is used for the empty list check because it is a programmer error (the function should never be called with an empty list in correct code). It's a precondition that we assume holds. However, if the list comes from user input, we might want to raise a ValueError instead. The type check and element type checks are for external validation that must always be enforced, so we use raise.
4. Consider the following code:
try:
try:
x = int("abc")
except ValueError as e:
raise TypeError("Conversion error") from e
except TypeError as e:
print(e)
print(e.__cause__)
print(e.__context__)
e, e.__cause__, and e.__context__?__cause__ and __context__.try:
try:
x = int("abc")
except ValueError as e:
raise TypeError("Conversion error") from e
except TypeError as e:
print(e) # "Conversion error"
print(e.__cause__) # The original ValueError instance
print(e.__context__) # None (since we used 'from', __context__ is not set)
e is the TypeError with message "Conversion error".e.__cause__ is the original ValueError (from the int conversion).e.__context__ is None because we used from, which explicitly sets the cause and suppresses the implicit context.Difference: __cause__ is explicitly set by raise ... from. __context__ is the implicit previous exception that would have been chained if no from was used. With from, __cause__ takes precedence and __context__ is usually set to None.
5. Design a validation function validate_user_input(data) that:
data with keys 'name', 'age', 'email'.def validate_user_input(data):
required_keys = {'name', 'age', 'email'}
if not all(key in data for key in required_keys):
raise KeyError(f"Missing keys. Required: {required_keys}")
name = data['name']
if not isinstance(name, str) or not name.strip():
raise ValueError("Name must be a non-empty string")
age = data['age']
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0 or age > 120:
raise ValueError("Age must be between 0 and 120")
email = data['email']
if not isinstance(email, str) or '@' not in email:
raise ValueError("Email must contain '@'")
return True
# Test
test_data = [{'name': 'Alice', 'age': 30, 'email': 'alice@example.com'},
{'name': '', 'age': 25, 'email': 'bob@example.com'},
{'name': 'Bob', 'age': 150, 'email': 'bob@example.com'},
{'name': 'Charlie', 'age': 'twenty', 'email': 'charlie@test'}]
for data in test_data:
try:
validate_user_input(data)
print("Valid")
except (KeyError, ValueError, TypeError) as e:
print(f"Invalid: {e}")
This tutorial provides a thorough understanding of raising exceptions with raise. The quizzes, exercises, and homework problems will help you master these concepts and apply them in real-world programming. Happy coding!