This document covers debugging Python programs using assert statements including assertion syntax, sanity checks, precondition validation, and best practices for catching bugs early in development. Proactive bug detection technique.
This document explores debugging Python programs using assert statements as sanity checks to catch bugs early in development. Assertions validate assumptions, check preconditions, verify intermediate states, and provide clear error messages when conditions fail, enabling proactive bug detection throughout the coding process.
A developer’s worst nightmare is spending hours developing code only to discover multiple bugs right before deployment. Instead of waiting until the last minute to check code correctness, developers should test and validate throughout the development process. Assert statements provide a mechanism for these continuous sanity checks.
Assertions are logical tests that developers use as sanity checks when writing code. In Python, the assert statement writes these sanity checks with one fundamental principle: the condition should always be true. If the condition is false, it indicates a bug in the program.
When an assert statement evaluates to false:
AssertionError exception is raised1assert condition
1assert condition, "Error message"
| Component | Description | Required |
|---|---|---|
assert keyword | Starts the assertion statement | Yes |
condition | Boolean expression to test | Yes |
message | Error message if condition fails | No |
When the condition is true, no error is raised:
1a = 3 + 4
2assert a == 7
3print("Assertion passed, program continues")
Output:
1Assertion passed, program continues
When the condition is false, program terminates:
1a = 3 + 4
2assert a == 8 # This will fail!
3print("This line never executes")
Output:
1Traceback (most recent call last):
2 File "example.py", line 2, in <module>
3 assert a == 8
4AssertionError
Provide helpful context when assertion fails:
1a = 3 + 4
2assert a == 8, f"Expected 8, but got {a}"
Output:
1Traceback (most recent call last):
2 File "example.py", line 2, in <module>
3 assert a == 8, f"Expected 8, but got {a}"
4AssertionError: Expected 8, but got 7
Ensure function receives valid input:
1def read_file_and_do_something(filename):
2 assert filename != "", "You must specify a filename!"
3
4 with open(filename, "r") as fp:
5 content = fp.read()
6 # Process content...
7
8 return content
9
10# This works
11result = read_file_and_do_something("data.txt")
12
13# This raises AssertionError with clear message
14result = read_file_and_do_something("") # AssertionError: You must specify a filename!
Verify conditions before performing operations:
1def divide(numerator, denominator):
2 assert denominator != 0, "Denominator cannot be zero!"
3 return numerator / denominator
4
5def calculate_average(numbers):
6 assert len(numbers) > 0, "Cannot calculate average of empty list!"
7 return sum(numbers) / len(numbers)
8
9def get_item(items, index):
10 assert 0 <= index < len(items), f"Index {index} out of range [0, {len(items)-1}]"
11 return items[index]
1def process_user(user_id, username):
2 assert isinstance(user_id, int), f"user_id must be int, got {type(user_id)}"
3 assert isinstance(username, str), f"username must be str, got {type(username)}"
4 assert len(username) > 0, "username cannot be empty"
5
6 # Process user...
7 print(f"Processing user {user_id}: {username}")
8
9process_user(123, "alice") # Works
10process_user("123", "alice") # AssertionError: user_id must be int, got <class 'str'>
1def set_temperature(temp_celsius):
2 assert -273.15 <= temp_celsius <= 1000, f"Temperature {temp_celsius}°C is physically impossible"
3 print(f"Temperature set to {temp_celsius}°C")
4
5def set_percentage(value):
6 assert 0 <= value <= 100, f"Percentage must be between 0 and 100, got {value}"
7 print(f"Value set to {value}%")
8
9def set_age(age):
10 assert 0 <= age <= 150, f"Age {age} is not realistic"
11 assert isinstance(age, int), "Age must be an integer"
12 print(f"Age set to {age}")
1def calculate_total_with_tax(subtotal, tax_rate):
2 assert subtotal >= 0, "Subtotal cannot be negative"
3 assert 0 <= tax_rate <= 1, f"Tax rate must be between 0 and 1, got {tax_rate}"
4
5 tax = subtotal * tax_rate
6 total = subtotal + tax
7
8 # Verify calculation makes sense
9 assert total >= subtotal, "Total should be at least equal to subtotal"
10
11 return total
1def binary_search(arr, target):
2 assert len(arr) > 0, "Array cannot be empty"
3 assert all(arr[i] <= arr[i+1] for i in range(len(arr)-1)), "Array must be sorted"
4
5 left, right = 0, len(arr) - 1
6
7 while left <= right:
8 # Invariant: target is in arr[left:right+1] if it exists
9 assert 0 <= left <= len(arr), "Left boundary out of range"
10 assert 0 <= right < len(arr), "Right boundary out of range"
11
12 mid = (left + right) // 2
13
14 if arr[mid] == target:
15 return mid
16 elif arr[mid] < target:
17 left = mid + 1
18 else:
19 right = mid - 1
20
21 return -1 # Not found
| Benefit | Description |
|---|---|
| Early bug detection | Catches bugs immediately when assumptions violated |
| Prevents cascading errors | Stops execution before invalid state propagates |
| Documentation | Expresses assumptions explicitly in code |
| Debugging aid | Identifies exact location where assumption fails |
| Development-time checks | Helps during development without production overhead |
| Clear error messages | Custom messages explain what went wrong |
| Aspect | Assertions | Exceptions |
|---|---|---|
| Purpose | Check internal correctness | Handle expected errors |
| When to use | Development and testing | Production code |
| Can be disabled | Yes (with -O flag) | No |
| User input validation | No | Yes |
| Programming errors | Yes | Sometimes |
| Recovery possible | No | Yes |
1def calculate_discount(price, discount_rate):
2 assert 0 <= discount_rate <= 1, "Discount rate must be between 0 and 1"
3 return price * (1 - discount_rate)
1def calculate_discount(price, discount_rate):
2 if not (0 <= discount_rate <= 1):
3 raise ValueError("Discount rate must be between 0 and 1")
4 return price * (1 - discount_rate)
| Scenario | Use Assert | Use If-Raise |
|---|---|---|
| Internal function calls | ✓ | |
| Developer-controlled values | ✓ | |
| User input validation | ✓ | |
| External API data | ✓ | |
| Production error handling | ✓ | |
| Development sanity checks | ✓ |
Important
Never use assertions for validating user input or external data. Assertions can be disabled with Python’s
-Ooptimization flag, making them unreliable for production error handling.
1def create_user(username, email, age):
2 assert username and email and age, "All parameters are required"
3 assert isinstance(username, str) and len(username) >= 3, "Username must be string with at least 3 characters"
4 assert '@' in email and '.' in email, "Email must be valid format"
5 assert isinstance(age, int) and age >= 18, "Age must be integer >= 18"
6
7 print(f"User created: {username}, {email}, {age}")
1def process_matrix(matrix):
2 # Check it's a 2D list
3 assert isinstance(matrix, list), "Matrix must be a list"
4 assert all(isinstance(row, list) for row in matrix), "Matrix must be 2D list"
5
6 # Check all rows have same length
7 if len(matrix) > 0:
8 row_length = len(matrix[0])
9 assert all(len(row) == row_length for row in matrix), "All rows must have same length"
10
11 # Check all elements are numbers
12 assert all(
13 all(isinstance(val, (int, float)) for val in row)
14 for row in matrix
15 ), "All matrix elements must be numbers"
16
17 print(f"Processing {len(matrix)}x{len(matrix[0]) if matrix else 0} matrix")
1def find_maximum(numbers):
2 assert len(numbers) > 0, "Cannot find maximum of empty list"
3
4 max_val = numbers[0]
5
6 for num in numbers:
7 assert isinstance(num, (int, float)), f"Expected number, got {type(num)}"
8
9 if num > max_val:
10 max_val = num
11
12 # Invariant: max_val is the largest value seen so far
13 assert all(max_val >= n for n in numbers[:numbers.index(num)+1]), "Invariant violated"
14
15 return max_val
Assertions are disabled when Python runs in optimized mode:
1# Normal mode - assertions enabled
2python3 script.py
3
4# Optimized mode - assertions disabled
5python3 -O script.py
6
7# Check if assertions are enabled
8python3 -c "print(__debug__)" # True
9python3 -O -c "print(__debug__)" # False
| Reason | Explanation |
|---|---|
| Performance | Remove overhead in production |
| Development tool | Assertions are primarily for development |
| Separate concerns | Runtime errors should use exceptions |
Warning
Because assertions can be disabled, never use them for:
- Validating user input
- Security checks
- Error handling in production
- Operations with side effects (e.g.,
assert f() == valuewhere f() modifies state)
| Do | Don’t |
|---|---|
| Use for development-time checks | Use for production error handling |
| Check internal invariants | Validate user input |
| Document assumptions | Rely on them for security |
| Add descriptive messages | Put side effects in assertions |
| Test boundary conditions | Use as control flow |
| Verify preconditions | Catch AssertionError exceptions |
1# Bad - no context
2assert x > 0
3
4# Better - explains what's wrong
5assert x > 0, "x must be positive"
6
7# Best - includes actual value
8assert x > 0, f"x must be positive, got {x}"
9
10# Excellent - includes context and expected range
11assert 0 < x <= 100, f"x must be in range (0, 100], got {x}"
1def process_data(data, config):
2 # Preconditions at function start
3 assert data is not None, "Data cannot be None"
4 assert len(data) > 0, "Data cannot be empty"
5 assert isinstance(config, dict), "Config must be dictionary"
6
7 # Process data
8 result = transform_data(data)
9
10 # Check intermediate state
11 assert len(result) == len(data), "Transform should preserve length"
12
13 filtered = filter_data(result, config)
14
15 # Postcondition before return
16 assert filtered is not None, "Filter should not return None"
17
18 return filtered
1def read_file_and_do_something(filename):
2 with open(filename, "r") as fp:
3 content = fp.read()
4 return content
5
6# What happens with empty filename?
7result = read_file_and_do_something("") # Confusing FileNotFoundError
1def read_file_and_do_something(filename):
2 assert filename != ""
3
4 with open(filename, "r") as fp:
5 content = fp.read()
6 return content
7
8# Better, but error message is blank
9result = read_file_and_do_something("") # AssertionError (no message)
1def read_file_and_do_something(filename):
2 assert filename != "", "You must specify a filename!"
3 assert isinstance(filename, str), f"Filename must be string, got {type(filename)}"
4
5 with open(filename, "r") as fp:
6 content = fp.read()
7
8 assert content is not None, "File content should not be None"
9
10 return content
11
12# Clear, helpful error message
13result = read_file_and_do_something("") # AssertionError: You must specify a filename!
1def test_calculate_average():
2 # Test normal case
3 result = calculate_average([10, 20, 30])
4 assert result == 20, f"Expected 20, got {result}"
5
6 # Test single element
7 result = calculate_average([42])
8 assert result == 42, f"Expected 42, got {result}"
9
10 # Test floating point
11 result = calculate_average([1, 2, 3])
12 assert abs(result - 2.0) < 0.0001, f"Expected 2.0, got {result}"
13
14 print("All tests passed!")
15
16test_calculate_average()
Using assert statements in code enables checking that code works properly, detecting bugs early, and maintaining developer sanity. Assert statements should validate internal correctness assumptions during development but should not replace proper exception handling for user input or production error conditions. Effective assert messages clearly explain what went wrong and include actual values when relevant. Use assert statements throughout code to create robust and reliable implementations, saving time from extra debugging and code rewrites.
Assertions are powerful debugging tools that catch bugs early by validating assumptions throughout code execution. They serve as executable documentation of preconditions, postconditions, and invariants. When used correctly, assertions help developers build confidence in their code during development. However, assertions should complement, not replace, proper error handling with exceptions for production scenarios. The key is understanding when to use assertions for internal checks versus when to use exceptions for handling expected error conditions.