# How to Parameterize Tests with pytest


When a test function contains the right logic but needs to run against multiple inputs, copying the function and changing the values creates maintenance drag and hides which cases actually exist. `@pytest.mark.parametrize` solves this by generating a separate test case for each set of inputs from a single function definition.

## Prerequisites

A [uv](https://pydevtools.com/handbook/reference/uv.md)-managed project with [pytest](https://pydevtools.com/handbook/reference/pytest.md) as a dev dependency. If you haven't set that up yet, follow [Setting up testing with pytest and uv](https://pydevtools.com/handbook/tutorial/setting-up-testing-with-pytest-and-uv.md).

## Parametrize a Single Argument

Pass the parameter name as a string and the values as a list:

```python {filename="tests/test_math.py"}
import pytest

@pytest.mark.parametrize("n", [0, 1, 4, 9, 16])
def test_sqrt_of_perfect_square(n):
    result = n**0.5
    assert result == int(result)
```

```bash
uv run pytest tests/test_math.py -v
```

pytest runs `test_sqrt_of_perfect_square` five times, once per value of `n`. The `-v` flag shows each case on its own line with the parameter value in brackets.

## Parametrize Multiple Arguments

Separate parameter names with a comma and pass tuples of values:

```python {filename="tests/test_math.py"}
import pytest

@pytest.mark.parametrize("base,exponent,expected", [
    (2, 0, 1),
    (2, 3, 8),
    (10, 2, 100),
    (5, 3, 125),
])
def test_power(base, exponent, expected):
    assert base**exponent == expected
```

Each tuple maps positionally to the parameter names. A mismatch in length raises a collection error before any test runs.

## Add Custom Test IDs

By default, pytest builds test IDs from the parameter values. When those values are opaque (long strings, complex objects), custom IDs make output readable and let you target specific cases with `-k`.

Pass an `ids` list with one label per parameter set:

```python {filename="tests/test_slugify.py"}
import pytest

def slugify(text):
    return text.lower().strip().replace(" ", "-")

@pytest.mark.parametrize(
    "text,expected",
    [
        ("Hello World", "hello-world"),
        ("  Leading Spaces", "leading-spaces"),
        ("ALLCAPS", "allcaps"),
    ],
    ids=["lowercase-and-hyphen", "leading-spaces", "allcaps"],
)
def test_slugify(text, expected):
    assert slugify(text) == expected
```

For inline labeling, use `pytest.param` with the `id` keyword:

```python {filename="tests/test_slugify.py"}
import pytest

@pytest.mark.parametrize("text,expected", [
    pytest.param("Hello World", "hello-world", id="lowercase-and-hyphen"),
    pytest.param("  Leading Spaces", "leading-spaces", id="leading-spaces"),
    pytest.param("ALLCAPS", "allcaps", id="allcaps"),
])
def test_slugify(text, expected):
    assert slugify(text) == expected
```

Both approaches produce the same output:

```console
$ uv run pytest tests/test_slugify.py -v
tests/test_slugify.py::test_slugify[lowercase-and-hyphen] PASSED
tests/test_slugify.py::test_slugify[leading-spaces] PASSED
tests/test_slugify.py::test_slugify[allcaps] PASSED
```

Run a single case by name:

```bash
uv run pytest -k "allcaps"
```

## Generate All Combinations with Stacked Decorators

Stacking two `@pytest.mark.parametrize` decorators produces the cartesian product of their values:

```python {filename="tests/test_format.py"}
import pytest

@pytest.mark.parametrize("precision", [1, 2, 4])
@pytest.mark.parametrize("value", [0.1, 3.14159, 100.0])
def test_round_format(value, precision):
    result = round(value, precision)
    assert isinstance(result, float)
```

This generates 9 test cases (3 values x 3 precisions). Use this when two parameters are independent and every combination is a valid case.

## Mark Expected Failures in a Parameter Set

Use `pytest.param` with the `marks` keyword to flag cases that should fail:

```python {filename="tests/test_math.py"}
import pytest

@pytest.mark.parametrize("test_input,expected", [
    ("3+5", 8),
    ("2+4", 6),
    pytest.param("6*9", 42, marks=pytest.mark.xfail, id="hitchhiker"),
])
def test_eval(test_input, expected):
    assert eval(test_input) == expected
```

The `hitchhiker` case is reported as `xfail` instead of `FAILED`, keeping the rest of the suite green while documenting the known discrepancy.

## Parametrize a Fixture with `indirect`

When parameter values need transformation or setup before the test runs, `indirect=True` routes values through a fixture instead of injecting them directly:

```python {filename="tests/test_api.py"}
import pytest

@pytest.fixture
def db_connection(request):
    engine = request.param
    conn = {"engine": engine, "connected": True}
    yield conn
    conn["connected"] = False

@pytest.mark.parametrize("db_connection", ["sqlite", "postgres"], indirect=True)
def test_query(db_connection):
    assert db_connection["connected"]
    assert db_connection["engine"] in ("sqlite", "postgres")
```

pytest passes `"sqlite"` and `"postgres"` to the `db_connection` fixture via `request.param`. The fixture builds the connection object and yields it to the test. This keeps setup logic in fixtures where it belongs, and each parameter value still generates a separate test case.

To apply `indirect` to only some parameters, pass a list of names:

```python
@pytest.mark.parametrize("db_connection,query", [("sqlite", "SELECT 1")], indirect=["db_connection"])
def test_run_query(db_connection, query):
    assert db_connection["connected"]
    assert query == "SELECT 1"
```

Here `db_connection` goes through the fixture; `query` is injected as a plain string.

## Learn More

- [pytest parametrize documentation](https://docs.pytest.org/en/stable/how-to/parametrize.html) covers the full API including `pytest_generate_tests`
- [How to run tests using uv](https://pydevtools.com/handbook/how-to/how-to-run-tests-using-uv.md) covers filtering, output control, and persistent defaults
- [How to run tests in parallel with pytest-xdist](https://pydevtools.com/handbook/how-to/how-to-run-tests-in-parallel-with-pytest-xdist.md) distributes parametrized tests across CPU cores
- [pytest reference](https://pydevtools.com/handbook/reference/pytest.md)
