Learning Goals
3 min- Write one test that runs over many input/expected pairs.
- Give each case a readable
id. - Parametrize across multiple arguments and stack decorators.
- Turn an equivalence-partition table (Lesson 4) directly into a parametrized test.
Warm-Up · Stop Copy-Pasting Tests
5 min# ✗ repetitive — 4 near-identical tests def test_even_4(): assert is_even(4) is True def test_even_7(): assert is_even(7) is False def test_even_0(): assert is_even(0) is True def test_even_neg2(): assert is_even(-2) is True # ✓ one test, a table of cases import pytest @pytest.mark.parametrize("n, expected", [ (4, True), (7, False), (0, True), (-2, True), ]) def test_is_even(n, expected): assert is_even(n) == expected
@pytest.mark.parametrize generates one test per row of a table. Each runs and reports separately, so if case 3 fails you see exactly which inputs broke — but you wrote the logic once. This is "table-driven testing".
New Concept · parametrize
14 minBasic shape
@pytest.mark.parametrize("arg_names", [list_of_cases]) def test_x(arg_names): ...
$ pytest -v test_is_even[4-True] PASSED test_is_even[7-False] PASSED test_is_even[0-True] PASSED test_is_even[-2-True] PASSED
Notice the auto-generated ids in brackets — each case is its own test line.
Readable custom ids
@pytest.mark.parametrize("amount, expected", [ (50, 50), (200, 180), (1000, 800), ], ids=["no-discount", "10-percent", "20-percent"]) def test_discount(amount, expected): assert total_due(amount) == expected
test_discount[no-discount] PASSED test_discount[10-percent] PASSED test_discount[20-percent] PASSED
Parametrize errors too
import pytest @pytest.mark.parametrize("bad", [-1, "abc", None]) def test_factorial_rejects(bad): with pytest.raises((ValueError, TypeError)): factorial(bad)
Stacking = cartesian product
@pytest.mark.parametrize("x", [1, 2]) @pytest.mark.parametrize("y", [10, 20]) def test_add(x, y): assert add(x, y) == x + y # runs 4 combinations: (1,10) (1,20) (2,10) (2,20)
Combine with fixtures
@pytest.mark.parametrize("item, price", [("roti", 1.5), ("nasi", 8.0)]) def test_add_item(empty_cart, item, price): # fixture + params together empty_cart.add(item, 1, price) assert empty_cart.total() == price
Worked Example · A Partition Table → One Test
12 minRemember the signup boundary table from Lesson 4? It becomes a single parametrized test:
# test_signup.py import pytest from signup import can_signup @pytest.mark.parametrize("age, expected", [ (7, False), # too low (12, False), # boundary just below (13, True), # boundary on (30, True), # mid valid (120, True), # boundary on (upper) (121, False), # boundary just above (200, False), # too high ], ids=["too-low", "12", "13", "valid", "120", "121", "too-high"]) def test_can_signup(age, expected): assert can_signup(age) is expected
$ pytest test_signup.py -v test_can_signup[too-low] PASSED test_can_signup[12] PASSED test_can_signup[13] PASSED test_can_signup[valid] PASSED test_can_signup[120] PASSED test_can_signup[121] PASSED test_can_signup[too-high] PASSED 7 passed
Read the diff
Seven boundary cases, one test function, and each case reported by a clear id. If someone changes <= to < in can_signup, exactly the [120] case fails — pinpointing the regression. The Lesson-4 test-design skill plus parametrize is the professional workflow: design the table, encode it once.
Try It Yourself
13 minTake 4+ near-identical tests and merge them into one parametrized test. Confirm pytest reports each case separately.
Add ids=[...] so failures read like test_x[negative-input] instead of raw values.
Take any function with clear rules. Write its full partition + boundary table from Lesson 4, then encode it as a single parametrized test with named ids.
Mini-Challenge · Roman Numerals
8 minWrite to_roman(n) and test it with a parametrized table covering 1, 4, 9, 40, 90, 400, 900, and a big number like 1994 (MCMXCIV). One test, eight cases, named ids.
Show the test
import pytest from roman import to_roman @pytest.mark.parametrize("n, expected", [ (1, "I"), (4, "IV"), (9, "IX"), (40, "XL"), (90, "XC"), (400, "CD"), (900, "CM"), (1994, "MCMXCIV"), ]) def test_to_roman(n, expected): assert to_roman(n) == expected
The "subtractive" cases (4, 9, 40, 90, 400, 900) are where roman-numeral code usually breaks — parametrize forces you to cover them all.
Recap
3 min@pytest.mark.parametrize runs one test across a table of cases, each reported separately, so failures pinpoint the exact inputs. Add ids= for readable names; stack decorators for combinations; mix with fixtures. It's the natural home for your Lesson-4 partition/boundary tables. Next: markers for skipping and xfail.
Vocabulary Card
- parametrize
- Run one test function over many input/expected rows.
- case id
- The readable label pytest shows for each parametrized case.
- table-driven test
- Encoding many scenarios as data rather than repeated code.
- cartesian product
- All combinations produced by stacking parametrize decorators.
Homework
4 minPick a function with rich behaviour (a calculator, a grade banding, a string formatter). Write a parametrized test with ≥ 10 cases including all boundaries and error inputs, each with a readable id. Then break the function and confirm pytest names the exact failing case.
Use the signup boundary example as the model. Include negative/invalid inputs with pytest.raises (a separate parametrized test for the error cases).