Parametrization
@pytest.mark.parametrize runs one test function against many input/output pairs. One test definition, clear failure output per case.
Recipe
import pytest
@pytest.mark.parametrize("input,expected", [
(2, 4),
(3, 9),
(0, 0),
])
def test_square(input, expected):
assert square(input) == expectedWhen to reach for this:
- Same logic tested with multiple inputs
- Boundary value testing (0, -1, max)
- Replacing copy-pasted test functions
Working Example
import pytest
from myapp.billing import apply_discount
from decimal import Decimal
@pytest.mark.parametrize("price,pct,expected", [
(Decimal("100"), 10, Decimal("90")),
(Decimal("100"), 0, Decimal("100")),
(Decimal("50"), 50, Decimal("25")),
(Decimal("0"), 10, Decimal("0")),
])
def test_apply_discount(price, pct, expected):
assert apply_discount(price, pct) == expected
@pytest.mark.parametrize("pct", [-1, 101, 150])
def test_invalid_discount_raises(pct):
with pytest.raises(DiscountError):
apply_discount(Decimal("100"), pct)uv run pytest tests/test_billing.py -v
# test_apply_discount[100-10-90] PASSED
# test_apply_discount[100-0-100] PASSED
# test_invalid_discount_raises[-1] PASSEDWhat this demonstrates:
- Multiple (input, expected) tuples in one decorator
- Each tuple generates a separate test with a descriptive ID
- Separate parametrize for the error cases
- Failures show exactly which parameter set failed
Deep Dive
Custom Test IDs
@pytest.mark.parametrize("pct", [10, 20], ids=["ten-percent", "twenty-percent"])
def test_discount(pct): ...Combining Parametrize
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_combo(x, y): ... # 4 test runsPython Notes
# Parametrize with pytest.param for marks
@pytest.mark.parametrize("n", [
pytest.param(0, marks=pytest.mark.xfail(reason="zero edge case")),
1, 2, 3,
])Gotchas
- Too many cases in one parametrize - unreadable table. Fix: split by category or use a data file.
- No ids on similar inputs - hard to identify failing case. Fix: add
ids=or use readable values. - Mutating parametrize data - leaks between runs. Fix: treat parametrize values as immutable.
- Parametrize instead of hypothesis - misses edge cases you did not list. Fix: use Hypothesis for complex input domains.
- Combining parametrizes accidentally - Cartesian product explosion. Fix: be deliberate; 3x3x3 = 27 tests fast.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Hypothesis | Large input spaces | 3-5 known cases |
| Loop in test | Never | - |
| Separate test functions | Unrelated scenarios | Same logic, different inputs |
FAQs
How many parametrize cases is too many?
If the table is hard to read (>15 rows), split or use a CSV fixture.
Can I parametrize fixtures?
Yes: @pytest.fixture(params=[...]).
How do I skip one parameter set?
pytest.param(value, marks=pytest.mark.skip(reason="...")).
How do I name test cases?
ids=["case1", "case2"] or ids=lambda val: f"pct-{val}".
Can I load params from a file?
Yes. Read a CSV/YAML in conftest.py and pass to parametrize programmatically.
parametrize vs fixture params?
parametrize on test: explicit inputs. Fixture params: shared setup variations.
How do I parametrize class tests?
Put @pytest.mark.parametrize on the class or each method.
Does order matter?
No for correctness. Order affects test report sequence only.
Can I parametrize async tests?
Yes with pytest-asyncio. Same decorator on async def test_....
How do I see all generated test names?
pytest --collect-only lists every parametrized variant.
Related
- Assertions & Test Structure - test layout
- Property-Based Testing - generative inputs
- Fixtures - shared setup
- Testing Best Practices - when to parametrize
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.