Python - Assert

An assert statement is used for debugging and internal consistency checks.

Introduction

An assertion is a boolean expression at a specific point in a program that will be true unless there is a bug in the program.

An assertion could simply be a comment used by the programmer to think about how the code works. Or an assertion could document a constraint on the system. (See also: ExceptionsAsConstraints).

However, it is often possible to actually compile the assertion to code and let it be executed in context to see if the statement it makes really does hold. When programmers talk about assertions, they usually mean this kind of executed assertion.

Python provides the assert statement for this purpose.

The assert statement in Python is a debugging aid that tests a condition:

  • If the condition is true, nothing happens, and your program continues to run.
  • If the condition is false, Python raises an AssertionError exception, immediately stopping the program.

Here’s a simple example:

index = -100
items: list[int] = [1, 2, 3]
assert 0 <= index < len(items), f"{index} is out of bounds"

As long as the index is within the range specified, this statement is a no-op.

If the index is out of range, however, the assertion will terminate the application and display the failed boolean expression as well as the filename and line number of the assertion in the source code.

Traceback (most recent call last):
File "C:\Users\david\Workspace\uniprot\tmp.py", line 4, in <module>
assert 0 <= index < len(items), f"{index} is out of bounds"
^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: -100 is out of bounds

The diagnostic information printed by a failed assertion (not to mention the potentially dramatic act of terminating the offending program) is not very end-user-friendly.

This goes to an important point: assertions are not an error-handling mechanism. The purpose of an assertion is not to handle an error, it is to (ultimately) notify a programmer of an error so that he can fix it.

Since assertions that don’t fail are no-ops, once a program has been thoroughly tested and bug-fixed, it is possible to recompile the source code without the assertions to produce a program that is both smaller and faster; for programs that make heavy use of assertions, this can result in a significant difference in performance.

Note

When an assertion fails, the program halts, and a programmer figures out what’s wrong, and fixes it. Then the assertion doesn’t fail anymore. It’s important to understand that the code is not supposed to handle the error - a person is.

It is also important to remember that standard operating procedure for assertions is to turn them off before doing a build that will be sent to real customers.

Basic Syntax

The syntax for assert is simple:

assert condition, "Optional error message"
  • condition: An expression that Python evaluates to True or False.
  • message: (Optional) A string that will be displayed if the assertion fails.

How Assertions Work

Behind the scenes, an assert statement is equivalent to the following code:

if __debug__:
if not condition:
raise AssertionError(message)

The __debug__ Variable

Python has a built-in constant called __debug__.

  • By default, __debug__ is True.
  • When you run Python with the -O (optimize) flag, __debug__ becomes False, and all assertions are ignored.
  • The -OO flag does the same, but also removes docstrings from the compiled bytecode.

This makes assertions a “zero-cost” debugging tool in production environments where performance is critical.

Functions

Typically, most functions have a restricted domain of input values and only work within a certain subset of the possible states of the system. In any other configuration, the function is undefined, thus forming an error condition. The common methods of dealing with these conditions are through error codes, assertions and exceptions.

As these are the actual filters into the function, they serve to document the domain of the function. They also help reduce bugs (their primary purpose).

The constraints are such things as checking for None values and number values outside a given range

Practical Examples

1. Checking Function Inputs

One common use is to ensure that arguments passed to a function meet certain criteria during development.

def calculate_discount(price, discount):
assert 0 <= discount <= 1, f"Discount must be between 0 and 1, got {discount}"
return price * (1 - discount)
print(calculate_discount(100, 0.2)) # Works fine: 80.0
# print(calculate_discount(100, 1.5)) # Raises AssertionError: Discount must be between 0 and 1, got 1.5

2. Internal Consistency Checks

Use assertions to verify that your code is behaving as expected internally.

def get_user_status(user_id):
# Imagine fetching from a database
status = "active"
allowed_statuses = ["active", "inactive", "pending"]
assert status in allowed_statuses, f"Unknown status: {status}"
return status

3. Checking Data Types

While Python is dynamically typed, sometimes you want to be sure you’re working with the right type during development.

def process_items(items):
assert isinstance(items, list), f"Expected list, got {type(items).__name__}"
return [item.upper() for item in items]

Mini-Project: Simple Banking System

Let’s see how assert can be used in a small project to ensure internal consistency and catch logic errors early.

Imagine we are building a simple BankAccount class.

We use:

  • if statements for user-facing validation (like checking if there’s enough money)
  • assert for developer-facing internal logic checks (like ensuring the balance never goes negative due to a bug).
class BankAccount:
def __init__(self, owner, balance=0):
# User-facing validation
if balance < 0:
raise ValueError("Initial balance cannot be negative")
self.owner = owner
self.balance = balance
def deposit(self, amount):
# User-facing validation
if amount <= 0:
raise ValueError("Deposit amount must be positive")
old_balance = self.balance
self.balance += amount
# Internal consistency check
assert self.balance == old_balance + amount, "Deposit calculation failed!"
assert self.balance > old_balance, "Balance should have increased"
def withdraw(self, amount):
# User-facing validation
if amount > self.balance:
raise ValueError("Insufficient funds")
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
old_balance = self.balance
self.balance -= amount
# Internal consistency check: The balance should NEVER be negative here
# If it is, there is a fundamental bug in our math logic
assert self.balance >= 0, f"Balance dropped below zero: {self.balance}"
assert self.balance == old_balance - amount, "Withdrawal calculation failed!"
# Testing the project
account = BankAccount("Alice", 100)
account.deposit(50)
print(f"Balance: {account.balance}") # Balance: 150
account.withdraw(30)
print(f"Balance: {account.balance}") # Balance: 120
# account.withdraw(200) # Raises ValueError: Insufficient funds (Standard Error Handling)

In this project:

  • We use ValueError for things the user might do wrong (depositing negative money).
  • We use assert for things the developer might do wrong (failing at basic math or breaking internal invariants).

Project 2: Student Grade Management

This project demonstrates using assertions to maintain data integrity in a system that tracks student grades across multiple subjects.

class GradeSystem:
def __init__(self):
self.grades = {} # Format: {'Student Name': {'Subject': grade}}
def add_grade(self, student, subject, grade):
# User-facing validation
if not (0 <= grade <= 100):
raise ValueError("Grade must be between 0 and 100")
if student not in self.grades:
self.grades[student] = {}
self.grades[student][subject] = grade
# Internal consistency check: Verify the grade was actually stored
assert student in self.grades, "Student should exist in grades dictionary"
assert self.grades[student][subject] == grade, "Grade was not updated correctly"
def get_average(self, student):
# User-facing validation
if student not in self.grades:
raise KeyError(f"Student '{student}' not found")
student_grades = self.grades[student].values()
# Internal consistency check: A student in our system should have at least one grade
# if the logic for adding grades is correct and we don't allow empty student entries.
assert len(student_grades) > 0, f"Internal Error: Student '{student}' has no grades record"
average = sum(student_grades) / len(student_grades)
# Internal logic check: Average must be within the valid range
assert 0 <= average <= 100, f"Calculated average {average} is out of bounds!"
return average
# Testing the project
system = GradeSystem()
system.add_grade("Bob", "Math", 90)
system.add_grade("Bob", "Science", 80)
print(f"Bob's Average: {system.get_average('Bob')}") # Bob's Average: 85.0
# This would trigger an assertion if we had a bug that allowed empty records
# system.grades["Alice"] = {}
# print(system.get_average("Alice")) # Raises AssertionError

Handling AssertionError

When an assertion fails, Python raises an AssertionError. While you typically want the program to stop when an assertion fails (that’s the point of debugging!), you can also handle it using a try...except block if needed.

def divide(a, b):
try:
assert b != 0, "Division by zero is not allowed"
return a / b
except AssertionError as e:
print(f"Error: {e}")
return None
print(divide(10, 2)) # Output: 5.0
print(divide(10, 0)) # Output: Error: Division by zero is not allowed \n None

Note: In practice, for a divide function, it’s usually better to use a standard if check or let the ZeroDivisionError occur, but this shows how you can catch the exception.

Common Pitfalls

1. The Tuple Pitfall

This is the most common mistake with assert. If you pass a tuple as the first argument, the assertion will always evaluate to True, even if the condition inside the tuple is False.

# WRONG: Always evaluates to True because a non-empty tuple is truthy
assert (1 == 2, "This should fail but won't!")
# CORRECT: The condition and message should be separated by a comma, not wrapped in parentheses
assert 1 == 2, "This will fail correctly"

2. Side Effects

Be careful not to include expressions that have “side effects” inside an assertion. A side effect is something that changes the state of your program (like calling a function that modifies a variable).

# BAD: The item will only be popped if assertions are enabled!
# If run with python -O, this line is completely skipped.
assert my_list.pop() == "expected_value"
# GOOD: Perform the action first, then assert the result
item = my_list.pop()
assert item == "expected_value"

When NOT to use Assert

Assertions are powerful, but they have two crucial rules:

  1. Don’t use them for data validation: Never use assert to validate user input or data from an external source (like a web form). Use if statements and raise specific exceptions instead.

  2. Don’t use them for essential logic: Python can be run with the -O (optimize) flag, which disables all assertions. If your program relies on an assertion to perform a calculation or update a state, it will break in production.

Security Implications

Since assertions can be disabled, using them for security checks (like authentication or permission verification) is a major security risk. An attacker could run your code with the -O flag and bypass all security measures implemented with assert.

Assert vs. If/Raise

Featureassertif / raise
PurposeDebugging / Internal checksError handling / Data validation
Can be disabled?Yes (with -O or -OO flags)No
Intended AudienceDevelopersEnd users / Other modules

Pending

Task

Middle Element of a List

You have a function that calculates the middle element of a list. It should never be called with an empty list by other parts of your internal API.

def get_middle_element(items):
pass
assert get_middle_element([1, 2, 3]) == 2
get_middle_element([]) # Triggers AssertionError

This is a bad design because the caller can pass a list of even length, so there is no element in the middle of the list.

Whoever uses the function should not have to know in advance whether the list has a middle element or not, because the function itself is precisely the one that has to find it—if it exists.

Therefore, one option is to use an exception to handle this error case, but that would again be a destitute design.

The correct definition of the function should be:

def get_middle_element(items: list[int]) -> int | None:
pass
assert get_middle_item([1, 2, 3]) == 2
assert get_middle_item([1, 2]) is None
assert get_middle_item([]) is None
Task

Implement the function get_middle_element that returns the middle element of a list if its length is odd, or None otherwise.

Task

Task

Sum function sum all numbers in a list, and expect no None values.

def sum(items: list[int]) -> int:
pass
assert sum([1, 2, 3]) == 6
sum([1, None, 3]) # Triggers AssertionErro
Task

Add an assertion to check that the input score is within a valid range.

def get_grade(score: int) -> str:
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F"

Pending