Previous | Tutorial index | Next
assertassert statement to prevent future exceptions in a program.As a programmer, you make many assumptions when writing code: "This list will never be empty," "This variable will always be positive," "This key will always exist in the dictionary." But what if those assumptions are wrong? Instead of letting the program produce incorrect results or crash mysteriously, you can use assertions to explicitly check your assumptions.
The assert statement in Python is a powerful debugging tool that helps you catch logical errors early, during development. It allows you to test a condition and, if the condition is False, immediately raise an AssertionError. This stops the program and gives you a clear signal that something you thought was impossible actually happened.
Key Idea: Assertions are your safety net for catching bugs—they are not for handling expected runtime errors like invalid user input.
Before diving into the content, familiarise yourself with these key terms:
| Term | Definition |
|---|---|
| Assertion | A statement that tests a condition and raises an AssertionError if the condition is False. |
assert statement |
The Python syntax for assertions: assert condition, "message". |
| Precondition | A condition that must be true before a function executes. |
| Postcondition | A condition that must be true after a function returns. |
| Invariant | A condition that must always be true for a class or data structure. |
-O flag |
The Python command-line flag that disables assertions (optimize mode). |
__debug__ |
A built-in variable that is True when assertions are enabled, False when disabled. |
Imagine you are an architect designing a skyscraper. You have detailed blueprints (your program). Before the building is occupied, you hire a building inspector (your assert statements) to check that certain critical assumptions hold: "The foundation is at least 10 meters deep," "The steel beams are properly welded," etc. If any of these checks fail, the inspector raises a red flag (an AssertionError), and the building is not allowed to open. These checks are performed during the inspection phase (development). Once the building is certified (production), you don't re-inspect every beam—you trust that the assumptions are correct.
Similarly, assertions are meant to catch programming errors during development and testing. In production, they can be turned off for performance.
assert Statement: Syntax and Basic Usageassert condition, "Optional error message"
condition: A boolean expression that you expect to be True.error message: An optional string that is included in the AssertionError if the condition is False.If the condition is True, nothing happens and the program continues. If it is False, an AssertionError is raised with the provided message (or no message if omitted).
# Example 1: Simple assertion
x = 10
assert x > 0 # Passes, nothing happens
# Example 2: Assertion with message
x = -5
assert x > 0, "x must be positive" # Raises AssertionError: x must be positive
# Example 3: Assertion in a function
def square_root(x):
assert x >= 0, "Cannot compute square root of negative number"
return x ** 0.5
print(square_root(9)) # 3.0
# print(square_root(-1)) # AssertionError
When an assertion fails, Python raises an AssertionError. This is a built-in exception that inherits from Exception. You can catch it with try...except, but in practice, you rarely do so because assertions are meant to be used for debugging, not for recoverable error handling.
try:
assert 1 == 2, "Math is broken"
except AssertionError as e:
print(f"Caught: {e}")
However, catching AssertionError is not recommended in production code because it defeats the purpose of assertions as a debugging tool.
assertA precondition is a condition that must be true before a function executes. Assertions are an excellent way to document and enforce preconditions.
def divide(numerator, denominator):
assert denominator != 0, "Denominator cannot be zero"
return numerator / denominator
Postconditions are conditions that must be true after a function returns. You can use assertions to verify that your function produces the expected result.
def square_root(x):
assert x >= 0, "x must be non-negative"
result = x ** 0.5
assert result >= 0, "Postcondition: square root must be non-negative"
return result
Invariants are conditions that must always be true for a class or data structure. Assertions can be placed at the beginning and end of methods to ensure the invariant holds.
class BankAccount:
def __init__(self, balance):
assert balance >= 0, "Initial balance cannot be negative"
self.balance = balance
def deposit(self, amount):
assert amount > 0, "Deposit amount must be positive"
self.balance += amount
self._check_invariant()
def withdraw(self, amount):
assert amount > 0, "Withdrawal amount must be positive"
assert amount <= self.balance, "Insufficient balance"
self.balance -= amount
self._check_invariant()
def _check_invariant(self):
assert self.balance >= 0, "Invariant violated: balance cannot be negative"
Assertions are great for catching logical errors during development. For example, you might assert that a list is sorted after a sorting function:
def bubble_sort(arr):
# ... implementation ...
# After sorting, assert that the array is sorted
for i in range(len(arr)-1):
assert arr[i] <= arr[i+1], "Bubble sort failed to sort the array"
return arr
Assertions also serve as documentation. They make explicit what the programmer assumes about the state of the program. A reader can see, "Aha, this function expects a non-empty list" by looking at the assert.
assertAssertions can be globally disabled, so you should never use them for validating data that comes from outside your program. If the user enters an invalid age, you should raise a ValueError or use if statements, not an assert.
# Bad: Using assert for user input
age = int(input("Enter age: "))
assert age >= 0, "Age cannot be negative" # This may be disabled!
# Good: Using raise for user input
age = int(input("Enter age: "))
if age < 0:
raise ValueError("Age cannot be negative")
Because assertions can be disabled, they should never be relied upon for critical error handling that must always happen, such as security checks, resource cleanup, or business logic validation.
Assertions are checked only when they are enabled. If your assertion contains a function call that changes program state (a side effect), that change will not happen when assertions are disabled, leading to subtle bugs.
# Dangerous: assertion with side effect
assert pop_from_stack() == expected_value, "Popped wrong value"
# When assertions are disabled, pop_from_stack() is never called!
Always keep assertions side-effect-free.
Even when assertions are enabled, they add overhead. In performance-critical sections, you may want to avoid assertions or disable them in production.
Assertions can be globally disabled by running Python with the -O (optimize) or -OO (also removes docstrings) flag.
python -O script.py
When disabled, all assert statements are ignored—they are not executed and the conditions are not evaluated. This means that code inside the condition (including function calls) will not run, which is why side effects in assertions are dangerous.
Note: The -O flag also sets the internal variable __debug__ to False. You can check this variable to conditionally include code:
if __debug__:
print("Debug mode enabled")
else:
print("Optimized mode")
But you almost never need to do this.
assert vs raise: A Detailed Comparison| Feature | assert |
raise |
|---|---|---|
| Purpose | Debugging, testing assumptions during development | Runtime error signaling for any situation |
| Can be disabled | Yes, with -O flag |
No, always active |
| When to use | Conditions that should never happen if code is correct | Conditions that can happen due to invalid input, resource issues, business rules |
| Error type | AssertionError |
Any exception class (ValueError, TypeError, custom, etc.) |
| Message | Optional, but recommended for clarity | Required for custom exceptions (or can use default) |
| Use in production | Not recommended for validation | Recommended for all validation and error conditions |
| Documentation | Acts as a comment that is checked | Acts as a formal error specification |
Rule of Thumb:
assert for conditions that should not happen if your code is bug-free.raise for conditions that can happen due to external factors or misuse of the API.| Pitfall | Solution |
|---|---|
Using assert for user input or validation |
Use if and raise instead. |
| Assertion with side effects | Keep assertions side-effect-free. |
Putting assert in a tuple without parentheses |
assert (condition, "message") always succeeds because a non-empty tuple is truthy! Use assert condition, "message" (no parentheses). |
| Relying on assertions for security | Assertions can be disabled; use proper checks. |
| Not providing a message | Always include a descriptive message for debugging. |
| Overusing assertions (everywhere) | Use them only for critical assumptions; avoid clutter. |
Example of the tuple pitfall:
assert (x > 0, "x must be positive") # This always succeeds because the tuple is truthy!
# Correct:
assert x > 0, "x must be positive"
class Stack:
def __init__(self, max_size=None):
self.items = []
self.max_size = max_size
# Invariant: max_size > 0 if specified
if max_size is not None:
assert max_size > 0, "max_size must be positive"
def push(self, item):
# Precondition: stack must not be full if max_size is set
if self.max_size is not None:
assert len(self.items) < self.max_size, "Stack is full"
self.items.append(item)
def pop(self):
# Precondition: stack must not be empty
assert len(self.items) > 0, "Cannot pop from empty stack"
return self.items.pop()
def is_empty(self):
return len(self.items) == 0
def __len__(self):
return len(self.items)
Testing:
s = Stack(2)
s.push(1)
s.push(2)
# s.push(3) # AssertionError: Stack is full
print(s.pop()) # 2
print(s.pop()) # 1
# print(s.pop()) # AssertionError: Cannot pop from empty stack
| Concept | Description |
|---|---|
assert |
Checks a condition and raises AssertionError if False. |
| Purpose | Debugging aid, not runtime error handling. |
| Disabling | Use -O flag to disable all assertions. |
| Use cases | Preconditions, postconditions, invariants, debugging. |
| Do NOT use for | User input validation, production error handling, security checks. |
| Best practice | Always provide an error message; avoid side effects; document assumptions. |
Test your understanding of the concepts covered in this tutorial. Answer each question, then click to reveal the correct answer.
Q1: What is the primary purpose of the assert statement in Python?
if statements for data validation.Q2: What happens when an assert condition evaluates to False?
AssertionError is raised.SyntaxError is raised.Q3: True or False: Assertions are always active and cannot be disabled.
Q4: Which of the following is a correct use of assert?
assert user_age >= 0, "Age must be non-negative" (user input)assert len(numbers) > 0, "List cannot be empty" (internal function precondition)assert file_is_open, "File not open" (runtime condition that may occur)assert pop() == expected, "Wrong value" (contains side effect)Q5: How can you globally disable all assert statements in a Python program?
-D flag.-O flag.--disable-assert flag.DISABLE_ASSERT=1.Q6: Which exception is raised when an assertion fails?
ValueErrorTypeErrorAssertionErrorRuntimeErrorQ7: What is the output of the following code?
x = 5
assert x == 5, "x is not 5"
print("Done")
Q8: Which of the following is a pitfall when using assert?
Q9: If you have an assertion with a side effect (e.g., assert pop() == 42), what happens when assertions are disabled?
Q10: Which is more appropriate for checking that a user-provided password meets a length requirement?
assertraise with ValueErrorComplete the exercises below to reinforce your understanding. Sample solutions are provided after each exercise.
Instructions: The following function calculate_discount(price, discount_percent) is intended to apply a discount. Add appropriate assert statements to check:
price is a non-negative number.discount_percent is between 0 and 100.Write the function with these assertions and test it with valid and invalid inputs.
def calculate_discount(price, discount_percent):
# Preconditions
assert price >= 0, "Price must be non-negative"
assert 0 <= discount_percent <= 100, "Discount percent must be between 0 and 100"
final_price = price * (1 - discount_percent / 100)
# Postcondition
assert final_price >= 0, "Final price must be non-negative"
return final_price
# Test
print(calculate_discount(100, 20)) # 80.0
# print(calculate_discount(-10, 20)) # AssertionError
# print(calculate_discount(100, 120)) # AssertionError
Instructions: The following function find_max(nums) is supposed to return the maximum element in a list. However, it contains a bug. Add assertions to check preconditions and postconditions to help identify the bug.
def find_max(nums):
max_val = 0
for n in nums:
if n > max_val:
max_val = n
return max_val
Test it with [3, 5, 2] and [-1, -5, -3]. Why does the second test fail? Fix the bug and add appropriate assertions.
def find_max(nums):
# Precondition: list must not be empty
assert len(nums) > 0, "List cannot be empty"
max_val = nums[0] # Bug fixed: initialize with first element
for n in nums:
if n > max_val:
max_val = n
return max_val
# Test with assertions
# print(find_max([3, 5, 2])) # 5
# print(find_max([-1, -5, -3])) # -1
Explanation: The bug was initializing max_val = 0, which fails for negative numbers. The assertion on the empty list would catch an empty input.
Instructions: Write a class Temperature that represents a temperature in Celsius. It should:
celsius.to_fahrenheit() that returns the temperature in Fahrenheit.to_kelvin() that returns the temperature in Kelvin.to_kelvin() that the result is non-negative.Test the class with valid and invalid temperatures.
class Temperature:
ABSOLUTE_ZERO = -273.15
def __init__(self, celsius):
assert celsius >= Temperature.ABSOLUTE_ZERO, f"Temperature cannot be below absolute zero ({Temperature.ABSOLUTE_ZERO}°C)"
self.celsius = celsius
def to_fahrenheit(self):
return self.celsius * 9/5 + 32
def to_kelvin(self):
kelvin = self.celsius - Temperature.ABSOLUTE_ZERO
# Postcondition: Kelvin must be >= 0
assert kelvin >= 0, "Kelvin temperature cannot be negative"
return kelvin
# Test
t = Temperature(25)
print(t.to_fahrenheit()) # 77.0
print(t.to_kelvin()) # 298.15
# t = Temperature(-300) # AssertionError
Instructions: Rewrite the following code using raise instead of assert where appropriate, and keep assert only where it is used for debugging.
def process_data(data):
assert data is not None, "Data cannot be None" # For debugging
assert len(data) > 0, "Data must not be empty" # For debugging
# Validate user input (should always be checked)
assert all(isinstance(x, int) for x in data), "All elements must be integers" # User input?
# Some processing...
return sum(data) / len(data)
Explain your reasoning.
def process_data(data):
# Preconditions for debugging (programmer errors)
assert data is not None, "Data cannot be None"
assert len(data) > 0, "Data must not be empty"
# User input validation - must always be enforced
if not all(isinstance(x, (int, float)) for x in data):
raise TypeError("All elements must be numbers")
return sum(data) / len(data)
# Explanation: The first two are checking assumptions about the caller's code,
# which should never be violated if the caller is correct. The third is checking
# the nature of the data itself, which could be from an external source and must
# be validated at runtime.
Instructions: Write a program that uses an assert statement to check a condition. Run it normally, then run it with python -O to disable assertions. Observe the difference. Write a comment in your code explaining what you observed.
# save as test_assert.py
def test_assertion():
x = 10
assert x == 5, "x should be 5"
print("This line is not printed if assertion fails")
test_assertion()
Run:
python test_assert.py # Raises AssertionError
python -O test_assert.py # No assertion error, prints "This line..."
Comment in code: "When run with -O, the assertion is disabled, so the program continues as if the condition were true. This is useful for performance-critical production code where we trust our logic."
1. You are tasked with implementing a function safe_divide(a, b) that:
a / b.assert to check that b is not zero (as a precondition).assert to check that the result is finite (postcondition) by using math.isfinite.b is zero with raise; assume that the function is called only with valid b from internal code.Write the function and discuss why using assert here is appropriate. Then, provide a second version of the function that also uses raise for b == 0 and explain why that might be more flexible.
import math
# Version 1: Using assert (assuming internal use)
def safe_divide_assert(a, b):
assert b != 0, "Denominator cannot be zero"
result = a / b
assert math.isfinite(result), "Result must be finite"
return result
# Version 2: Using raise (more flexible)
def safe_divide_raise(a, b):
if b == 0:
raise ValueError("Denominator cannot be zero")
result = a / b
if not math.isfinite(result):
raise RuntimeError("Result is not finite")
return result
Discussion: Version 1 uses assert because it assumes the function is called only from internal code that should never pass zero. It's a debugging aid. Version 2 uses raise because it can be used in production with external input; it always checks the condition.
2. Examine the following code snippets. For each, determine whether the use of assert is appropriate. If not, suggest a better alternative.
a)
def load_configuration(file_path):
with open(file_path, 'r') as f:
content = f.read()
config = json.loads(content)
assert 'database' in config, "Missing 'database' key in config"
return config
b)
def process_payment(amount, balance):
assert amount > 0, "Amount must be positive"
assert amount <= balance, "Insufficient balance"
balance -= amount
return balance
c)
def sort_list(lst):
assert isinstance(lst, list), "Input must be a list"
lst.sort()
assert lst == sorted(lst), "Sorting failed"
return lst
d)
def connect_to_server(server):
assert ping(server), "Server is unreachable" # ping() is a function that changes state
# Connect...
a) Inappropriate. The config file is external; missing keys should be handled with raise (e.g., KeyError or custom exception). Use raise instead.
b) Partially appropriate. amount > 0 is a business rule that should always be enforced, so use raise ValueError. amount <= balance is also a business rule; use raise RuntimeError. Assertions are not appropriate here.
c) Appropriate. The first assert is a precondition that the caller should guarantee (type check). The second assert is a postcondition to catch sorting bugs. However, the type check could be considered validation if input comes from outside; but if it's internal, it's fine.
d) Inappropriate. ping(server) likely has side effects (network calls) and is a runtime check. If assertions are disabled, the connection attempt proceeds without checking reachability. Use raise with explicit validation.
3. Write a custom function my_assert(condition, message) that behaves like assert but does not use the built-in assert statement. It should:
False, raise an AssertionError with the given message.True, do nothing.ENABLE_ASSERT that you can set to True or False to enable/disable all assertions (similar to -O).Use this function instead of assert in a small program. Discuss the advantages and disadvantages compared to the built-in assert.
ENABLE_ASSERT = True
def my_assert(condition, message):
if ENABLE_ASSERT:
if not condition:
raise AssertionError(message)
# Usage
x = 5
my_assert(x == 10, "x should be 10")
Advantages:
ENABLE_ASSERT).Disadvantages:
assert (because of function call overhead).__debug__ and -O flag.4. Write a program that measures the execution time of a function with and without assertions. Use the timeit module. The function should perform a computation (e.g., summing a large list) with assertions inside. Compare the runtime with assertions enabled and disabled (using -O). Write a report summarizing your findings.
import timeit
def sum_with_assertions(lst):
total = 0
for x in lst:
assert x > 0, "Positive only"
total += x
return total
def sum_without_assertions(lst):
total = 0
for x in lst:
total += x
return total
lst = list(range(1000000))
# Measure with assertions enabled (run as normal)
# timeit.timeit('sum_with_assertions(lst)', globals=globals(), number=10)
# Measure with assertions disabled: run with python -O and import the functions.
Expected findings: Assertions add overhead; with -O the speed is closer to the version without assertions. The performance impact depends on the frequency of assertions and the complexity of the condition being checked.
5. Discuss the role of assertions in the software development lifecycle. Include:
Provide examples from real-world programming languages to support your arguments.
Assertions play a vital role in the software development lifecycle, serving as a first line of defense against logical errors. During development and testing, assertions help catch bugs early by validating assumptions that programmers make about the state of the program. They act as executable documentation, making explicit what the programmer expects to be true at certain points in the code. For example, a function that computes the square root might assert that its input is non-negative, immediately catching a bug if a negative value is passed during testing.
The relationship between assertions and unit tests is complementary. Unit tests verify the external behavior of functions, while assertions verify internal assumptions. A well-designed unit test suite can catch many bugs, but assertions provide additional safety by checking conditions that might not be covered by tests. For instance, an assertion that a list is sorted after a sorting function can catch subtle bugs that might not be detected by simple input-output tests.
Assertions are often turned off in production to improve performance. In performance-critical applications, the overhead of checking assertions can be significant, especially if they are placed inside loops. Additionally, once software is thoroughly tested, the probability of assertion failures is low, so disabling them is considered safe.
However, there are potential dangers in relying on assertions for correctness. If assertions are used for input validation or security checks, disabling them can expose the application to vulnerabilities. For example, if an assertion is used to check that a user has the appropriate permissions, disabling assertions would bypass the security check. Therefore, assertions should never be used for security-critical or input validation tasks.
My opinion is that assertions should be used liberally during development and testing, but disabled in production for performance. However, critical validation should always be implemented using raise statements that are never disabled. This balanced approach ensures that bugs are caught early in development while maintaining performance and security in production. Other languages like C, C++, and Java have similar assert mechanisms with the ability to disable them, reflecting a widely accepted best practice.
This tutorial provides a comprehensive understanding of the assert statement, its appropriate use cases, and its limitations. The quizzes, exercises, and homework problems will help reinforce the concepts and distinguish assert from raise. Happy coding!