Learning Goals
3 min- Read pytest's assertion failure output for ints, lists, dicts, strings.
- Use
pytest.approxfor float comparisons. - Use
pytest.raiseswithmatchfor exception messages. - Write assertions that produce the clearest failures.
Warm-Up · The Detective Output
5 mindef test_lists(): assert [1, 2, 3] == [1, 2, 4]
> assert [1, 2, 3] == [1, 2, 4] E assert [1, 2, 3] == [1, 2, 4] E At index 2 diff: 3 != 4 E Full diff: E - [1, 2, 4] E + [1, 2, 3]
You wrote one plain assert; pytest told you the exact index that differs and showed a diff. This introspection works for lists, dicts, sets, and strings — turning "something's wrong" into "index 2 is wrong". Learning to read it is a debugging superpower.
New Concept · Reading & Writing Good Asserts
14 minDicts — shows which key differs
assert got == {"name": "Aisyah", "age": 13}
E AssertionError: assert {'name': 'Aisyah', 'age': 14} == {'name': 'Aisyah', 'age': 13}
E Differing items:
E {'age': 14} != {'age': 13}Strings — shows the first differing character
assert slug == "hello-world" E AssertionError: assert 'hello_world' == 'hello-world' E - hello-world E + hello_world E ? ^
Floats — use pytest.approx
from pytest import approx def test_total(): assert cart_total([(9.99, 3)]) == approx(29.97) # or with a list: assert [0.1 + 0.2, 1.0] == approx([0.3, 1.0])
approx is pytest's float-safe equality — cleaner than math.isclose and it works inside lists and dicts too.
Exceptions — message matching
import pytest def test_withdraw_message(): with pytest.raises(ValueError, match="insufficient"): BankAccount(50).withdraw(100) # capture the exception to inspect it further: with pytest.raises(ValueError) as exc_info: BankAccount(50).withdraw(100) assert "100" in str(exc_info.value)
Write asserts that fail clearly
# ✗ vague — "False is not True" assert is_valid(data) # ✓ specific — shows the actual value assert is_valid(data) is True assert error_count == 0 # shows the actual count assert "error" not in response # shows the response on failure
One assert per logical fact. A test with ten asserts that fails on the third leaves the rest unchecked — split them or assert the whole structure at once.
Worked Example · Debugging from the Output Alone
12 min# test_parse.py from parse import parse_order def test_parse_order(): result = parse_order("2x roti @1.50") assert result == {"qty": 2, "item": "roti", "price": 1.50}
Run it and the bug reveals itself without you adding a single print:
> assert result == {"qty": 2, "item": "roti", "price": 1.50}
E AssertionError: assert {'qty': '2', 'item': 'roti', 'price': 1.5} == {'qty': 2, 'item': 'roti', 'price': 1.5}
E Differing items:
E {'qty': '2'} != {'qty': 2}The diff pinpoints it: qty is the string '2', not the int 2 — parse_order forgot to int() the quantity. You found the bug from the failure message alone.
# the fix in parse.py: def parse_order(text): qty_str, rest = text.split("x ", 1) item, price_str = rest.split(" @") return {"qty": int(qty_str), "item": item.strip(), "price": float(price_str)} # int() and float() added
Read the diff
This is the daily reality of working with pytest: a failing test's output usually tells you exactly what's wrong, so you rarely need print debugging. Reading the diff well — "string vs int", "index 2", "missing key" — is a skill that makes you fast.
Try It Yourself
13 minWrite four failing asserts (an int, a list, a dict, a string). Run each and read how pytest describes the difference.
Convert all float comparisons in a suite to pytest.approx, including one inside a list of values.
Have a friend introduce a subtle bug in a function. Run the tests and diagnose it purely from the failure output — no reading the code first. How fast can you go?
Mini-Challenge · Assert the Whole Structure
8 minYou have a function returning a list of dicts. Instead of looping with many asserts, assert the entire expected structure in one line and let pytest's introspection point to any mismatch. Compare the failure clarity to checking field-by-field.
Show the approach
def test_top_scores(): result = leaderboard([("A", 90), ("B", 70), ("C", 80)]) # one assert on the whole shape — pytest diffs it for you assert result == [ {"name": "A", "score": 90}, {"name": "C", "score": 80}, {"name": "B", "score": 70}, ]
A single structural assert gives a full diff and is far more readable than 9 separate field checks — pytest does the comparison work.
Recap
3 minpytest rewrites failing asserts into detailed diffs: which list index, which dict key, which character. Use pytest.approx for floats (works in lists/dicts too) and pytest.raises(Exc, match=...) for exceptions. Write specific asserts and prefer one structural assert over many tiny ones. Reading failure output well makes you a fast debugger. Next: fixtures.
Vocabulary Card
- assert introspection
- pytest's detailed reconstruction of why an assert failed.
- pytest.approx
- Float-tolerant equality, usable in lists and dicts.
- match=
- Regex check on an exception's message inside pytest.raises.
- structural assert
- Asserting a whole data structure at once and letting pytest diff it.
Homework
4 minWrite a suite for a function returning a complex value (a dict, a list of dicts, or a tuple). Use one structural assert per test, pytest.approx for floats, and pytest.raises(match=...) for errors. Break the function and confirm pytest pinpoints the exact mismatch.
Model on the leaderboard structural-assert example. The grader looks for whole-structure asserts and approx on float fields.