Python assert
last modified October 30, 2026 Python 3.15
In this article we show how to work with assertions in Python. We define assertions, explain the difference between assertions and exceptions and show their relation to unit tests.
Assertions
Assertions are internal self-checks in the code; they help developers find bugs as soon as possible. The purpose of assertions is to detect problems early in the program, where the cause is clear, rather than letting them pop up later as a side effect of some other operation.
Assertions should not be used for data processing or validation, because they can be turned off. Some programming languages, such as Go, do not have assertions. Other languages have assert statements, but they are used less often (Java, where they are disabled by default).
Assertions vs exceptions
Assertions are boolean expressions that must be true for the code to be correct. An assertion failure means there is a bug, and the fix is to correct the code. Exceptions indicate non-typical situations that can occur at runtime even in correct code. For instance, if the Internet connection is down, or the file permissions are not sufficient, the program has to deal with it. Assertions should not be used for handling runtime errors.
Design by contract
Design by contract, or programming by contract, is a software development practice in which developers define formal, precise and verifiable interface specifications for software components. The specifications are divided into preconditions, postconditions and invariants.
A precondition is a predicate that must always be true prior to the execution of a piece of code. A postcondition is a predicate that must always be true after the execution of a piece of code. An invariant is a property which remains unchanged after all operations.
The design by contract approach was developed in the 1980s by Bertrand Meyer for the Eiffel language and was inspired by business contracts.
Assertions and unit tests
Assertions and unit tests overlap in some areas, but there are some differences. Assertions are assumptions about the internal state of the program, while unit tests examine the external behavior of a module. Unit tests work with test data, while assertions do not.
Assert statements are used in unit tests. Classic assertions are assumptions about something which is true, while in unit tests, developers often test for false conditions as well.
Python assert
The assert statement is a convenient way to insert debugging
assertions into a Python program. It consists of a boolean condition and an
optional error message, separated by a comma.
assert condition, message
If the condition is false, Python raises an AssertionError with
the message. The message expression is evaluated only when the assertion
fails. The statement is equivalent to the following code.
if __debug__:
if not condition:
raise AssertionError(message)
The built-in __debug__ constant is True unless
Python runs with the -O or -OO option, or with the
PYTHONOPTIMIZE environment variable set. In optimized mode, the
compiler removes assert statements entirely.
Python assert precondition
A precondition checks the arguments of a function before it does its work.
def info(age, name):
assert 18 <= age <= 65, 'age value must be between values 18 and 65'
print(f'{name} is {age} years old')
info(34, 'John')
info(84, 'Peter')
In the example, we have a precondition about the age value. The chained
comparison 18 <= age <= 65 checks that the value lies in
the range.
info(84, 'Peter')
The second call violates the precondition. The traceback shows where the assertion failed and the carets point to the failing condition.
$ python precondition.py
John is 34 years old
Traceback (most recent call last):
File "/home/user/python/precondition.py", line 9, in <module>
info(84, 'Peter')
~~~~^^^^^^^^^^^^^
File "/home/user/python/precondition.py", line 3, in info
assert 18 <= age <= 65, 'age value must be between values 18 and 65'
^^^^^^^^^^^^^^^
AssertionError: age value must be between values 18 and 65
Python assert postcondition
A postcondition checks the result of a function before it is returned.
def do_discount(price, discount):
discounted_price = price - discount * price
assert 0 < discounted_price < price, \
f'discounted price {discounted_price} out of range'
return discounted_price
print(do_discount(120, 0.2))
try:
print(do_discount(120, 1.2))
except AssertionError as e:
print(f'AssertionError: {e}')
The example has a postcondition about a discounted price. It has to be greater than zero and lower than the original price.
assert 0 < discounted_price < price, \
f'discounted price {discounted_price} out of range'
The message can be any expression, including an f-string that shows the offending value. A long assert statement can be continued on the next line with a backslash.
print(do_discount(120, 1.2))
A discount of 120% produces a negative price, so the postcondition fails. We
catch the AssertionError only to show the message; normally,
assertion errors are not caught.
$ python postcondition.py 96.0 AssertionError: discounted price -24.0 out of range
Python assert invariant
An invariant is a condition that holds for an object after every operation. The next class keeps its items sorted and checks this after each change.
import bisect
class SortedList:
def __init__(self):
self._items = []
def _check(self):
assert all(a <= b for a, b in zip(self._items, self._items[1:])), \
f'items not sorted: {self._items}'
def add(self, value):
bisect.insort(self._items, value)
self._check()
def add_fast(self, value):
self._items.append(value)
self._check()
def __repr__(self):
return f'SortedList({self._items})'
vals = SortedList()
for n in [5, 1, 4, 2]:
vals.add(n)
print(vals)
vals.add_fast(3)
The _check method asserts that each item is less than or equal
to the next one.
def add(self, value):
bisect.insort(self._items, value)
self._check()
The add method inserts the value at the right position with
bisect.insort, so the invariant holds.
def add_fast(self, value):
self._items.append(value)
self._check()
The add_fast method has a bug: it appends the value to the end.
The assertion catches the bug immediately, in the method that caused it.
$ python invariant.py
SortedList([1, 2, 4, 5])
Traceback (most recent call last):
File "/home/user/python/invariant.py", line 32, in <module>
vals.add_fast(3)
~~~~~~~~~~~~~^^^
File "/home/user/python/invariant.py", line 19, in add_fast
self._check()
~~~~~~~~~~~^^
File "/home/user/python/invariant.py", line 10, in _check
assert all(a <= b for a, b in zip(self._items, self._items[1:])), \
~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: items not sorted: [1, 2, 4, 5, 3]
Assertions are removed with -O
Since assertions can be disabled, they must never be used to validate input or to guard against conditions that can happen in correct programs.
def withdraw(balance, amount):
assert amount > 0, 'amount must be positive'
return balance - amount
print(__debug__)
print(withdraw(100, -50))
The withdraw function wrongly uses an assertion to reject
negative amounts. We also print the __debug__ constant.
We run the program normally and with the -O option. In
optimized mode, __debug__ is False, the assertion
is gone, and the function happily withdraws a negative amount.
$ python withdraw.py
True
Traceback (most recent call last):
File "/home/user/python/withdraw.py", line 7, in <module>
print(withdraw(100, -50))
~~~~~~~~^^^^^^^^^^
File "/home/user/python/withdraw.py", line 2, in withdraw
assert amount > 0, 'amount must be positive'
^^^^^^^^^^
AssertionError: amount must be positive
$ python -O withdraw.py
False
150
Validating input with exceptions
Input validation belongs in regular code that raises an exception. The checks then work regardless of the interpreter options.
def withdraw(balance, amount):
if amount <= 0:
raise ValueError(f'amount must be positive, got {amount}')
if amount > balance:
raise ValueError('insufficient funds')
return balance - amount
for amount in [30, -50, 500]:
try:
print(withdraw(100, amount))
except ValueError as e:
print(f'error: {e}')
The function raises a ValueError for a non-positive amount and
for an amount that exceeds the balance. The caller can handle these errors.
See Python exceptions.
$ python withdraw_checked.py 70 error: amount must be positive, got -50 error: insufficient funds
The tuple pitfall
The assert statement is not a function. Putting the condition
and the message in parentheses creates a tuple, and a non-empty tuple is
always truthy.
x = -1
assert (x > 0, 'x must be positive')
print('the assertion did not fail')
The assertion never fails, even though x is negative. Python
detects the mistake at compile time and emits a SyntaxWarning.
$ python tuple_pitfall.py /home/user/python/tuple_pitfall.py:3: SyntaxWarning: assertion is always true, perhaps remove parentheses? assert (x > 0, 'x must be positive') the assertion did not fail
Python assertions in unit tests
Python has two widely used unit test frameworks: the third-party
pytest and the standard library unittest. While
pytest uses the plain assert statement,
unittest favours methods such as assertEqual and
assertLess.
We test the following module.
def find_max(values):
_max = values[0]
for val in values:
if val > _max:
_max = val
return _max
def find_min(values):
_min = values[0]
for val in values:
if val < _min:
_min = val
return _min
We have two algorithms in a module. The functions are called
find_max and find_min so that they do not shadow
the built-in max and min functions.
Pytest example
The following example uses the pytest framework. It is
installed with pip install pytest.
import algo
def test_min():
values = (2, 3, 1, 4, 6)
val = algo.find_min(values)
assert val == 1
def test_max():
values = (2, 3, 1, 4, 6)
val = algo.find_max(values)
assert val == 6
We pass test data to the tested algo functions and check the
expected values with the assert statement. Pytest collects
functions whose names start with test from files named
test_*.py or *_test.py.
$ pytest test_algo.py ============================= test session starts ============================== platform linux -- Python 3.15.2, pytest-9.2.0, pluggy-1.7.0 rootdir: /home/user/python plugins: anyio-4.15.1 collected 2 items test_algo.py .. [100%] ============================== 2 passed in 0.01s ===============================
Pytest assertion introspection
Pytest rewrites assert statements in test modules. When an assertion fails, it shows the values of the subexpressions, so we do not need to write messages.
def find_max(values):
_max = 0
for val in values:
if val > _max:
_max = val
return _max
def stats(values):
return {'min': min(values), 'max': max(values), 'count': len(values) - 1}
def test_max_negative():
values = [-4, -2, -7]
assert find_max(values) == -2
def test_stats():
values = [2, 3, 1, 4, 6]
assert stats(values) == {'min': 1, 'max': 6, 'count': 5}
The module contains two buggy functions. The find_max function
starts with zero instead of the first element, and stats
miscounts the values.
assert find_max(values) == -2
For the first test, pytest shows that find_max returned 0.
assert stats(values) == {'min': 1, 'max': 6, 'count': 5}
For dictionaries, pytest reports which items differ.
$ pytest test_failing.py
============================= test session starts ==============================
platform linux -- Python 3.15.2, pytest-9.2.0, pluggy-1.7.0
rootdir: /home/user/python
plugins: anyio-4.15.1
collected 2 items
test_failing.py FF [100%]
=================================== FAILURES ===================================
______________________________ test_max_negative _______________________________
def test_max_negative():
values = [-4, -2, -7]
> assert find_max(values) == -2
E assert 0 == -2
E + where 0 = find_max([-4, -2, -7])
test_failing.py:19: AssertionError
__________________________________ test_stats __________________________________
def test_stats():
values = [2, 3, 1, 4, 6]
> assert stats(values) == {'min': 1, 'max': 6, 'count': 5}
E AssertionError: assert {'min': 1, 'm...6, 'count': 4} == {'min': 1, 'm...6, 'count': 5}
E
E Omitting 2 identical items, use -vv to show
E Differing items:
E {'count': 4} != {'count': 5}
E Use -v to get more diff
test_failing.py:25: AssertionError
=========================== short test summary info ============================
FAILED test_failing.py::test_max_negative - assert 0 == -2
FAILED test_failing.py::test_stats - AssertionError: assert {'min': 1, 'm...6...
============================== 2 failed in 0.02s ===============================
Unittest example
Now we test the same module with unittest.
import unittest
import algo
class AlgoTest(unittest.TestCase):
def test_min(self):
values = (2, 3, 1, 4, 6)
val = algo.find_min(values)
self.assertEqual(val, 1)
def test_max(self):
values = (2, 3, 1, 4, 6)
val = algo.find_max(values)
self.assertEqual(val, 6)
def test_empty(self):
with self.assertRaises(IndexError):
algo.find_max([])
if __name__ == '__main__':
unittest.main()
In the example, we use the assertEqual method. The
assertRaises context manager checks that
find_max raises an IndexError for an empty list.
The -v option prints the name of each test.
$ python test_algo_unittest.py -v test_empty (__main__.AlgoTest.test_empty) ... ok test_max (__main__.AlgoTest.test_max) ... ok test_min (__main__.AlgoTest.test_min) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.000s OK
Source
Python assert statement - language reference
In this article we have worked with the Python assert statement and explained assertions in general. See also Python pytest and Python unittest.
Author
List all Python tutorials.