Quick checks with assert
20 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)After you trace a bug once, write a test so it cannot return. An assert is the smallest test in Python. This lesson covers assert, tables of cases, property checks and the float trap.
assert says what must be true
assert is a statement that checks a condition and raises AssertionError, with an optional message, when the condition is false. It is used to test your own code.
After you write a function, write down what it must return. An assert statement checks that claim and stops the program with an error if it is false. When it is true, nothing happens.
def total(prices): return sum(prices)
assert total([10, 20]) == 30assert total([]) == 0assert total([-5, 5]) == 0, "negatives must cancel"print("all good")Example 1 · Three asserts and a message
def largest(values): return max(values)
assert largest([3, 9, 4]) == 9assert largest([-2, -7]) == -2assert largest([6]) == 6print("3 checks passed")3 checks passed
What this code does
- No output appears from a passing assert; the final print shows the run reached the end.
- The all-negative and one-item cases are in the list on purpose.
Each assert holds a condition. The text after the comma is shown only when the condition is false.
A table of cases
Writing one assert per case gets long. Put the inputs and the expected answers in a list of pairs, and check them in one loop. A new case is then one new line.
def clamp(x, low, high): return max(low, min(x, high))
cases = [ ((5, 0, 10), 5), ((-3, 0, 10), 0), ((99, 0, 10), 10), ((0, 0, 10), 0),]for args, expected in cases: got = clamp(*args) assert got == expected, f"{args}: got {got}, expected {expected}"print("passed", len(cases))Example 2 · A runner that reports every failure
def double(x): return x * x
cases = [(2, 4), (3, 6), (0, 0), (5, 10)]failed = 0for x, expected in cases: got = double(x) if got != expected: failed += 1 print(f"FAIL double({x}): got {got}, expected {expected}")print(failed, "failed of", len(cases))FAIL double(3): got 9, expected 6 FAIL double(5): got 25, expected 10 2 failed of 4
What this code does
- A plain assert stops at the first failure. This runner counts them all.
- The bug here is
x * xwherex * 2was meant; it passes for 2 and 0.
The message names the failing input and both values, so a failure tells you what to trace. *args unpacks the tuple into the three parameters.
Check properties, not only values
For some functions you cannot write every expected answer. You can still check facts that must hold. A sorted list must be in order and must keep the same items.
import random
random.seed(3)for _ in range(200): data = [random.randint(-9, 9) for _ in range(random.randint(0, 8))] out = sorted(data) assert len(out) == len(data) assert all(out[i] <= out[i + 1] for i in range(len(out) - 1)) assert sorted(out) == sorted(data)print("properties hold")Example 3 · Properties of a deduplicate function
def dedupe(items): seen = set() out = [] for x in items: if x not in seen: seen.add(x) out.append(x) return out
for data in ([], [1], [2, 2, 2], [3, 1, 3, 2, 1]): out = dedupe(data) assert len(out) == len(set(data)) assert set(out) == set(data) assert all(data.index(a) < data.index(b) for a, b in zip(out, out[1:])) print(data, "->", out)[] -> [] [1] -> [1] [2, 2, 2] -> [2] [3, 1, 3, 2, 1] -> [3, 1, 2]
What this code does
- Three facts must hold: no duplicates, no lost items and the first-seen order.
- The loop covers empty, one item, all equal and a mixed list.
Three properties together catch a lost item, a wrong order and a changed value. The seed makes any failure repeatable.
assert is for tests, not for user input
Python can run with -O, which removes every assert. Use assert to test your own code and to state things that must be true. For bad input from a user, raise an exception instead.
def safe_div(a, b): if b == 0: raise ValueError("b must not be zero") return a / b
try: safe_div(4, 0)except ValueError as err: print("caught:", err)A raised ValueError always runs. An assert might be removed, so it must never guard real input.
Compare floats with math.isclose
Decimal fractions are stored in binary, so some sums are not exact. Never assert that two floats are equal with ==. Ask whether they are close.
import math
assert 0.1 + 0.2 != 0.3assert math.isclose(0.1 + 0.2, 0.3)print(0.1 + 0.2)Example 4 · Floats need isclose
import math
print(0.1 + 0.2 == 0.3)print(math.isclose(0.1 + 0.2, 0.3))print(round(0.1 + 0.2, 2) == 0.3)False True True
What this code does
- Direct
==is False because 0.1 + 0.2 is stored as 0.30000000000000004. - Both
iscloseand rounding give True;iscloseis the clearer choice.
math.isclose allows a tiny relative error, which is what you want for averages and prices.
Next step
Check your understanding
11 quick questions