Using pytest.mark.parametrize to Run One Test Across Many Input Sets
A practical guide to pytest.mark.parametrize: reduce test duplication by running one function across many input sets with independent reporting, readable IDs, and selective execution.
01 Sept 2026, 16:33 UTC

Desired Outcome
Execute a single test function against multiple input combinations while producing independent test items for each case. Each parameter set gets its own pass/fail status, node ID, and selective execution capability via -k or node ID. This eliminates duplicated test bodies and improves coverage visibility.
Prerequisites
- pytest installed in the active environment (
pip install pytestor via your project's dependency manager). - A test module discoverable by pytest's default collection rules (file named
test_*.pyor*_test.py, functions prefixed withtest_). - Test function parameters that match the argument names declared in the
parametrizedecorator.
Procedure
Basic Decoration
Import pytest and apply the decorator with a comma-separated argument name string and a list of tuples:
import pytest
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_add(a, b, expected):
assert a + b == expected
Run the file or specific node IDs:
pytest test_math.py
pytest test_math.py::test_add[1-2-3]
Readable Case IDs
Default IDs like test_add[0], test_add[1] are opaque. Supply an ids list or use pytest.param for descriptive labels:
# Option 1: ids list (matches tuple order)
@pytest.mark.parametrize(
"a, b, expected",
[(1, 2, 3), (0, 0, 0), (-1, 1, 0)],
ids=["positive", "zeros", "negative"]
)
def test_add(a, b, expected):
assert a + b == expected
# Option 2: pytest.param with explicit id
@pytest.mark.parametrize("a, b, expected", [
pytest.param(1, 2, 3, id="positive"),
pytest.param(0, 0, 0, id="zeros"),
pytest.param(-1, 1, 0, id="negative"),
])
def test_add(a, b, expected):
assert a + b == expected
Resulting node IDs: test_add[positive], test_add[zeros], test_add[negative].
Multiple Decorators (Cartesian Product)
Stack decorators to combine independent parameter dimensions:
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_product(x, y):
assert x * y in (10, 20, 40)
Produces four cases: (1,10), (1,20), (2,10), (2,20).
Indirect Parametrization via Fixtures
When a parameter name matches a fixture, set indirect=True to pass the value to the fixture instead of the test function directly:
# conftest.py
@pytest.fixture
def connection_config(request):
# request.param receives each tuple from parametrize
host, port = request.param
return f"{host}:{port}"
# test_db.py
@pytest.mark.parametrize("connection_config", [
("localhost", 5432),
("db.example.com", 5432),
], indirect=True)
def test_connection(connection_config):
assert ":" in connection_config
Expected Checks
- Collection count: Run
pytest --collect-only test_module.pyand verify the output shows one test item per parameter tuple. - Independent execution: Use
pytest -k "positive"orpytest test_module.py::test_add[positive]to run a single case; it should pass or fail without affecting others. - Failure isolation: Introduce a deliberate failure in one tuple (e.g.,
(2, 2, 5)) and confirm only that node ID fails while the rest pass. - ID stability: Re-run collection after edits; descriptive IDs should remain attached to the same logical cases.
Recovery Options
| Symptom | Cause | Fix |
|---|---|---|
Collection error: ValueError: number of names... | Argument name count doesn't match tuple length | Count commas in the first decorator argument; ensure each tuple has the same number of elements. |
Unreadable IDs like test_func[0] | No ids supplied and default indexing used | Add ids=[...] list or wrap tuples in pytest.param(..., id=...). |
| Only some cases fail, hard to identify which | Default numeric IDs | Use descriptive IDs; run pytest -v to see full node IDs in output. |
| Fixture lookup error with indirect | Fixture name doesn't match parameter name or indirect=True missing | Ensure fixture name equals the parameter name in parametrize and indirect=True is set. |
| Shared mutable state between cases | Mutable default argument or reused object in parameter list | Use factories (e.g., lambda: []) or fixtures for per-case setup. |
Practical Decisions
Prefer Parametrize Over Loops
A loop inside a single test function stops at the first assertion failure and reports only one result. Parametrization runs each case independently, so you see exactly which inputs fail and can re-run just those.
Avoid Mutable Defaults in Parameter Lists
Don't write [(1, []), (2, [])] where the same list instance is reused. Instead, generate fresh objects per case:
# Bad: shared list
@pytest.mark.parametrize("data", [([],), ([],)])
def test_bad(data):
data.append(1) # leaks across cases
# Good: factory via pytest.param or helper
@pytest.mark.parametrize("data", [
pytest.param([], id="case1"),
pytest.param([], id="case2"),
])
def test_good(data):
data.append(1) # independent per case
Large Parameter Sets
Hundreds of cases slow collection and clutter reports. Consider:
- Splitting into multiple test functions by logical group.
- Generating tests dynamically with
pytest_generate_testsandmetafunc.parametrizefor programmatic control. - Using
@pytest.mark.parametrizewith a generator expression if cases are computed at collection time.
Fixtures for Expensive Setup, Parametrize for Variations
If each case needs a database connection, create a fixture that yields the connection and parametrize only the query parameters. Don't parametrize the connection itself unless you're testing multiple connection configurations (in which case use indirect parametrization).
Verification Checklist
pytest --collect-onlyshows expected number of items (e.g., 4 cases = 4 collected tests).pytest -k "partial_id"selects and runs only matching cases.- One injected failure causes exactly one node ID to fail; others pass.
- Node IDs are descriptive enough to diagnose without opening source.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.