Learning Goals
3 min- Use
assert conditionandassert condition, message. - Read an
AssertionErrorand fix what it points to. - Compare floats safely with
math.isclose. - Assert that an exception is raised, and know when NOT to use
assert.
Warm-Up · True or Stop
5 minassert 2 + 2 == 4 # nothing happens — it's true assert 2 + 2 == 5 # AssertionError! execution stops here
Traceback (most recent call last): ... AssertionError
assert X means "X must be true; if not, raise AssertionError immediately". Pass when true, explode when false. Every test framework is just a fancy wrapper around this one idea.
New Concept · assert, Done Right
14 minAdd a message — your future self will thank you
score = 105 assert 0 <= score <= 100, f"score out of range: {score}"
AssertionError: score out of range: 105
A bare assert x == y that fails just says "AssertionError". Add a message showing the actual value — debugging gets ten times faster.
Comparing floats — never use ==
assert 0.1 + 0.2 == 0.3 # ✗ FAILS! floats are imprecise print(0.1 + 0.2) # 0.30000000000000004 import math assert math.isclose(0.1 + 0.2, 0.3) # ✓ correct way
Floating-point arithmetic has tiny rounding errors. Always compare floats with math.isclose (or a tolerance), never ==.
Asserting an exception is raised
def withdraw(balance, amount): if amount > balance: raise ValueError("insufficient funds") return balance - amount # negative test: it MUST raise try: withdraw(50, 100) assert False, "expected ValueError, but none was raised" except ValueError: pass # good
The assert False trick: if the code didn't raise, we force a failure. (Frameworks have assertRaises / pytest.raises for this — Lessons 8 & 13.)
The danger: assert can be switched off
python -O myscript.py # the -O flag REMOVES all asserts!
# ✗ NEVER use assert for real program logic / security: assert user.is_admin, "access denied" # vanishes under python -O! # ✓ use a real check + exception for production logic: if not user.is_admin: raise PermissionError("access denied")
Rule: assert is for tests and internal sanity checks, never for validating user input or enforcing security in production code.
Worked Example · A Hand-Rolled Test Run
12 min# test_cart.py — a tiny test script using only assert import math def cart_total(items, tax=0.06): subtotal = sum(price * qty for price, qty in items) return round(subtotal * (1 + tax), 2) # positive cases assert cart_total([(10.0, 2), (5.0, 1)]) == 26.50, "basic total wrong" assert cart_total([]) == 0.0, "empty cart should be 0" # float-safe comparison assert math.isclose(cart_total([(9.99, 3)], tax=0), 29.97), "no-tax case" # negative quantity should still compute (or you decide to reject — test it!) assert cart_total([(10.0, 0)]) == 0.0, "zero quantity" print("✅ all cart tests passed")
✅ all cart tests passed
Now break the function on purpose and watch the message pinpoint it:
def cart_total(items, tax=0.06): subtotal = sum(price + qty for price, qty in items) # BUG: + not * return round(subtotal * (1 + tax), 2)
AssertionError: basic total wrong
Read the diff
The message "basic total wrong" told you instantly which expectation broke. Without messages you'd just see "AssertionError" and have to hunt. This little script is a real test suite — and it's exactly what unittest/pytest automate from Lesson 7 on.
Try It Yourself
13 minWrite 4 asserts for a function, each with a helpful failure message. Break the function and confirm the message tells you which one failed.
Show that assert 0.1 * 3 == 0.3 fails, then fix it with math.isclose.
Hint
print(0.1 * 3) # 0.30000000000000004 assert math.isclose(0.1 * 3, 0.3) # passes
Write a helper assert_raises(exc_type, func, *args) that returns True if calling func(*args) raises exc_type, else raises AssertionError.
Hint
def assert_raises(exc_type, func, *args): try: func(*args) except exc_type: return True raise AssertionError(f"expected {exc_type.__name__}") assert_raises(ValueError, withdraw, 50, 100)
Mini-Challenge · A Tiny Test Runner
8 minWrite a function run_tests(tests) that takes a list of zero-arg test functions, runs each, catches AssertionError, and prints a summary (how many passed / failed and which ones). This is the seed of every test framework.
Show one possible solution
def run_tests(tests): passed = failed = 0 for t in tests: try: t() print(f" ✅ {t.__name__}") passed += 1 except AssertionError as e: print(f" ❌ {t.__name__}: {e}") failed += 1 print(f"\n{passed} passed, {failed} failed") def test_add(): assert 2 + 2 == 4 def test_broken(): assert 2 + 2 == 5, "math is broken" run_tests([test_add, test_broken])
Non-negotiables: catch AssertionError so one failure doesn't stop the others, print per-test status + a summary. You just built unittest in 10 lines.
Recap
3 minassert X, msg raises AssertionError with your message if X is false. Always add a message showing the actual value. Compare floats with math.isclose, never ==. Assert exceptions with try/except. And never use assert for production logic or security — it vanishes under python -O. Next: writing a real test script without a framework.
Vocabulary Card
- assert
- Statement that raises AssertionError if its condition is false.
- AssertionError
- The exception a failed assert raises.
- math.isclose
- Compares floats within a tolerance — the right way to test float equality.
- -O flag
- Python's optimise flag; strips all asserts — so don't rely on them in production.
Homework
4 minTake the test-case table from Lesson 4's homework and turn every row into an assert with a message. Include at least one float comparison (with isclose) and one exception assertion. Run it; deliberately break the function once to confirm the messages guide you.
Convert each test-case row to assert func(input) == expected, f"case X: ...". Use math.isclose wherever the result is a float.