pytest parametrize: Run Tests with Multiple Inputs
Test duplication is one of the quietest forms of technical debt. You write a test for one input, copy it for another, tweak a value, repeat. Weeks later you have twelve nearly identical functions that all need updating when the code changes. pytest.mark.parametrize exists to stop this pattern before it starts.
What Parametrize Is and Why It Matters
@pytest.mark.parametrize is a built-in pytest decorator that runs a single test function multiple times, each time with a different set of inputs. Instead of writing one test per case, you write the test logic once and hand pytest a table of inputs and expected outputs.
The benefits go beyond less code. Each case gets its own result in the test report, so a failure on one input does not hide passes on others. You can skip or mark individual cases as expected failures without touching the test body. And adding a new case is a one-line change.
Basic Syntax — Single Parameter
The decorator takes two arguments: the name of the parameter (as a string) and a list of values.
import pytest
def double(n):
return n * 2
@pytest.mark.parametrize("n", [1, 5, 10, -3, 0])
def test_double(n):
assert double(n) == n * 2Pytest generates five tests from this — one for each value. The test IDs in the output will be test_double[1], test_double[5], and so on, making failures easy to spot.
If your function under test has a clear expected output per input, pass both together as a tuple:
@pytest.mark.parametrize("n, expected", [
(1, 2),
(5, 10),
(10, 20),
(-3, -6),
(0, 0),
])
def test_double(n, expected):
assert double(n) == expectedThe string "n, expected" is a comma-separated list that maps to the tuple positions. This is the most common pattern and reads like a data table.
Multiple Parameters in One Decorator
You are not limited to two values per case. Any number of parameters can be passed as long as the string and the tuples match in length.
def clamp(value, low, high):
return max(low, min(high, value))
@pytest.mark.parametrize("value, low, high, expected", [
(5, 1, 10, 5),
(0, 1, 10, 1),
(15, 1, 10, 10),
(1, 1, 10, 1),
(10, 1, 10, 10),
])
def test_clamp(value, low, high, expected):
assert clamp(value, low, high) == expectedKeeping the column alignment in the list makes the table readable at a glance. When a case fails, you immediately see which combination of boundary conditions triggered it.
Multiple Decorators — Cartesian Product
When you stack two @pytest.mark.parametrize decorators, pytest runs every combination of values from both. This is the cartesian product.
@pytest.mark.parametrize("base", [2, 10])
@pytest.mark.parametrize("exponent", [0, 1, 2])
def test_power(base, exponent):
assert base ** exponent == pow(base, exponent)This produces six tests: (base=2, exponent=0), (base=2, exponent=1), (base=2, exponent=2), and the same three for base=10. Use stacked decorators when you genuinely want all combinations. Use a single decorator with explicit tuples when only specific combinations make sense — the cartesian product can grow quickly and generate cases that are not meaningful.
pytest.param — IDs and Marks per Case
By default, pytest generates test IDs from the parameter values. For complex objects, the IDs become unreadable. pytest.param lets you set a human-readable ID for any case.
@pytest.mark.parametrize("payload", [
pytest.param({"user": "alice", "role": "admin"}, id="admin-user"),
pytest.param({"user": "bob", "role": "viewer"}, id="viewer-user"),
pytest.param({}, id="empty-payload"),
])
def test_can_parse_payload(payload):
result = parse_payload(payload)
assert result is not NoneThe id keyword replaces the auto-generated label in the test report.
pytest.param also accepts a marks argument, so you can apply skip or xfail to individual cases without a conditional inside the test:
import pytest
@pytest.mark.parametrize("n, expected", [
(2, 4),
pytest.param(0, 0, marks=pytest.mark.xfail(reason="division edge case")),
pytest.param(-1, -2, marks=pytest.mark.skip(reason="negative input not yet supported")),
])
def test_double(n, expected):
assert double(n) == expectedThe xfail case runs but is expected to fail. If it unexpectedly passes, pytest flags it as XPASS. The skip case is never executed. Both are reported separately from genuine failures.
Indirect Parametrize — Passing Params Through Fixtures
Sometimes you want the parameter to be processed by a fixture before it reaches the test. The indirect argument tells pytest to route the value through a fixture of the same name instead of injecting it directly.
import pytest
@pytest.fixture
def user(request):
role = request.param
return {"name": "test-user", "role": role, "token": f"tok-{role}"}
@pytest.mark.parametrize("user", ["admin", "editor", "viewer"], indirect=True)
def test_user_has_token(user):
assert user["token"].startswith("tok-")Pytest passes "admin", "editor", and "viewer" to the user fixture via request.param. The fixture builds and returns the full object. The test receives the built object, not the raw string.
You can make only some parameters indirect by passing a list instead of True:
@pytest.mark.parametrize("user, permission", [
("admin", "write"),
("viewer", "read"),
], indirect=["user"])
def test_permission(user, permission):
assert can_perform(user, permission)Here user goes through the fixture; permission is injected as a plain string.
Dynamic Parametrize with conftest Hooks
Hard-coding parameter lists works for most cases, but sometimes the inputs come from an external source — a file, a database query, or a configuration that varies across environments. The pytest_generate_tests hook in conftest.py lets you build the parameter list at collection time.
# conftest.py
def pytest_generate_tests(metafunc):
if "username" in metafunc.fixturenames:
users = load_users_from_config() # returns a list of strings
metafunc.parametrize("username", users)# test_login.py
def test_login(username):
result = login(username, password="test-pass")
assert result["success"] is Truemetafunc.parametrize works identically to the decorator version. You can pass ids, indirect, and scope as keyword arguments. This approach keeps the test file clean and moves the data-loading logic into one place.
Common Mistakes and Tips
Forgetting to match tuple length. If your parameter string is "a, b, c" but a tuple has only two elements, pytest raises a ValueError at collection time, not at runtime. Count your columns.
Mutable defaults in parameter lists. Dicts and lists in a parameter list are shared across test runs if pytest reuses the same object. Use pytest.param or construct fresh objects inside the test or fixture.
Too many cartesian combinations. Stacking three decorators with five values each gives 125 tests. Most will be redundant. Prefer explicit tuples for boundary conditions over exhaustive grids.
Ignoring indirect for complex setup. If a parametrized test needs more than one or two lines of setup, the logic belongs in a fixture. Use indirect=True rather than duplicating setup inside the test body.
Missing IDs on complex objects. When a parameter is a dict, class instance, or other non-primitive, pytest's auto-generated ID is something like test_foo[payload0]. Add id= to every pytest.param that wraps a complex object.
Parametrize at the class level. You can decorate a test class with @pytest.mark.parametrize, and every method in the class will receive the parametrized values. This is useful when a group of tests all depend on the same varying input.
@pytest.mark.parametrize("locale", ["en", "fr", "de"])
class TestFormatting:
def test_date(self, locale):
assert format_date("2024-01-15", locale) != ""
def test_currency(self, locale):
assert format_currency(1234.5, locale) != ""Each locale runs both methods, giving six tests total from one decorator.
pytest.mark.parametrize is one of the most practical tools in the pytest API. It removes test duplication, keeps failure reports granular, and scales to hundreds of cases without making the test file harder to read. Start with the single-parameter form, graduate to explicit tuples when you need expected outputs, and reach for pytest.param and indirect fixtures when cases need individual treatment or heavier setup.
Beyond Parametrized Tests
Parametrize scales your unit tests. For end-to-end browser tests that verify real user workflows, HelpMeTest generates and runs tests automatically with 24/7 monitoring.