Parametrize patterns
@pytest.mark.parametrize runs the same test function with multiple inputs. Each input becomes a separately reported test.
Basic shape
@pytest.mark.parametrize("n,expected", [
(0, 0),
(1, 1),
(5, 25),
])
def test_square(n, expected):
assert n * n == expected
- First arg: comma-separated parameter names (or a list of strings).
- Second arg: list of value tuples (one per case).
Pytest generates three tests with IDs like test_square[0-0], test_square[1-1], test_square[5-25].
Stacked parametrize — cartesian product
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", ["a", "b"])
def test_combo(x, y):
...
Runs four tests: (1, "a"), (1, "b"), (2, "a"), (2, "b"). Outer decorator iterates faster (closer to the function).
pytest.param — per-case marks and IDs
@pytest.mark.parametrize("input,expected", [
pytest.param(0, 1, id="zero"),
pytest.param(1, 2, id="one"),
pytest.param(-1, 0, id="negative", marks=pytest.mark.xfail),
pytest.param(None, None, id="none", marks=pytest.mark.skip(reason="not supported yet")),
])
def test_increment(input, expected):
assert increment(input) == expected
pytest.param(...):
id="..."— readable name in output (test_increment[zero]instead oftest_increment[0-1]).marks=pytest.mark.xfail— expected failure for this case only.marks=pytest.mark.skip(reason="...")— skip this case only.
You can stack marks: marks=[pytest.mark.slow, pytest.mark.xfail].
ids — readable test output
@pytest.mark.parametrize(
"user",
[User("alice", admin=True), User("bob", admin=False)],
ids=["admin", "regular"],
)
def test_permission(user):
...
Without ids, pytest tries to render objects — test_permission[user0], test_permission[user1] is what you get for non-primitive values.
ids can also be a callable:
def make_id(val):
return f"u_{val.name}_admin{val.admin}"
@pytest.mark.parametrize("user", users, ids=make_id)
def test_permission(user): ...
Parametrizing fixtures
@pytest.fixture(params=["sqlite", "postgres"])
def db(request):
return connect(request.param)
def test_query(db):
assert db.execute("SELECT 1").scalar() == 1
Every test that uses db runs twice. Combine with pytest.mark.parametrize and you get the full cartesian product:
@pytest.mark.parametrize("query", ["SELECT 1", "SELECT 2"])
def test_query(db, query):
# runs 4 times: 2 backends × 2 queries
...
See 03_fixtures_deep_dive.md for params and indirect.
Indirect — through the fixture
@pytest.fixture
def db(request):
backend = request.param
db = connect(backend)
yield db
db.close()
@pytest.mark.parametrize("db", ["sqlite", "postgres"], indirect=True)
def test_query(db):
...
Same effect as a parametrized fixture, but parametrization lives next to the test rather than in a fixture. Use this when only some tests need the param.
indirect=["db"] to mark only some args as indirect when there are multiple.
Loading test data from a file
import json
from pathlib import Path
cases = json.loads(Path("tests/cases.json").read_text())
@pytest.mark.parametrize(
"case",
cases,
ids=[c["name"] for c in cases],
)
def test_endpoint(case):
response = call(case["input"])
assert response == case["expected"]
Useful for golden-file / table-driven tests.
Generating cases programmatically
def pytest_generate_tests(metafunc):
if "code" in metafunc.fixturenames:
codes = list(Path("snippets").glob("*.py"))
metafunc.parametrize("code", codes, ids=[c.stem for c in codes])
pytest_generate_tests is a hook in conftest.py. Use when parameter values aren’t known until collection time (file discovery, env-dependent).
Conditional parametrize
import sys
cases = [
pytest.param("a", id="ascii"),
pytest.param("ä", id="unicode", marks=pytest.mark.skipif(sys.platform == "win32", reason="not supported")),
]
@pytest.mark.parametrize("text", cases)
def test_text(text): ...
Common pitfalls
- Mutable values shared across cases: each case gets the same dict if you do
[{"k": []}, {"k": []}]but mutate it — use deepcopy or factory. - Empty parametrize list:
@pytest.mark.parametrize("x", [])skips silently. Pytest 7+ warns; older silently passes. Usepytest.skipexplicitly if intentional. - IDs collide: pytest dedupes by appending
0,1, … to repeated IDs. Hard to read — give explicit unique IDs. - Type coercion in IDs:
pytest.param(1, "1")produces[1-1]— both indistinguishable. Useidsto disambiguate.
Interview angle
- Q: “How do you run the same test with multiple inputs?” —
@pytest.mark.parametrize. - Q: “How do you mark a single parametrize case as expected to fail?” —
pytest.param(..., marks=pytest.mark.xfail). - Follow-up: “Difference between parametrizing a fixture and using
indirect=True?” — fixture-level vs test-level location of the parameter list. Indirect lets you transform values via fixture logic. - Follow-up: “How do you generate cases at collection time?” —
pytest_generate_testshook.