ZetCode

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).

Note: Go designers explain that developers should think more about proper error handling and reporting. Go has assertions in unit tests only.

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.

precondition.py
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.

postcondition.py
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.

invariant.py
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.

withdraw.py
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.

withdraw_checked.py
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.

tuple_pitfall.py
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.

algo.py
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.

test_algo.py
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.

test_failing.py
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.

test_algo_unittest.py
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.