Back to Blog
Python

Using pytest.mark.parametrize for Parameterized Tests in Python

Learn how to use pytest.mark.parametrize to write parameterized tests in Python. Covers syntax, multiple arguments, fixtures, custom IDs, class/module-level use, and common pitfalls.

pytestparameterized testingpython testingtest parametrizationpytest.mark.parametrize
A Python test function decorated with pytest.mark.parametrize, showing multiple input sets feeding into a single test.

When you need to run the same test with multiple inputs, copying the test function is error-prone. pytest.mark.parametrize lets you define a test once and feed it a list of argument sets; pytest then runs the test once for each set.

The Purpose of pytest.mark.parametrize

Parametrized testing allows you to exercise a function with different inputs and expected outputs without duplicating test code. Instead of writing a separate test for each case, you declare the test once and provide a list of argument tuples. Pytest then generates one test case per tuple, and each case is reported independently. That means a failure in one case does not hide the results of the others.

Basic Syntax and Usage

The simplest form applies @pytest.mark.parametrize to a test function. The decorator takes the name of the argument (or a comma-separated string of names) and a list of values. For a single argument, the list contains the values directly.

import pytest def add(a, b): return a + b @pytest.mark.parametrize("a,b,expected", [ (1, 2, 3), (0, 0, 0), (-1, 1, 0), ]) def test_add(a, b, expected): assert add(a, b) == expected

Here, the test runs three times, once for each tuple. The a, b, and expected parameters are filled from the tuple. If any case fails, pytest reports it with the specific input values, making it easy to identify the failing combination.

Parametrizing with Multiple Arguments

When your test needs more than one argument, you can pass a comma-separated string of names to the decorator and provide a list of tuples. Each tuple must have the same length as the number of names. This is useful when you want to test a function that takes several inputs and produces a known output.

@pytest.mark.parametrize("text, separator, expected", [ ("a,b,c", ",", ["a", "b", "c"]), ("one two three", " ", ["one", "two", "three"]), ]) def test_split(text, separator, expected): assert text.split(separator) == expected

The names in the decorator string are bound positionally to the values in each tuple, so keep their order aligned with the parameter order of the test function.

Using Parametrize with Fixtures

Parametrize can also be combined with fixtures. With indirect=True, the argument names are treated as fixture names, and the values from the parameter list are passed to those fixtures. This lets you create different fixture setups for each test case.

import pytest @pytest.fixture def user(request): return request.param @pytest.mark.parametrize("user", ["alice", "bob"], indirect=True) def test_user_name(user): assert user in ["alice", "bob"]

Here, the user fixture receives the value from the parametrize list. This is useful when you need to set up resources that depend on the test case, such as database connections or file handles. The fixture can also return a different object based on the parameter.

Controlling Test IDs for Readable Output

By default, pytest generates test IDs from the parameter values. For example, test_add[1-2-3]. When values are complex or contain spaces, the IDs become hard to read. You can provide custom IDs with the ids argument, either as a list of strings or as a callable that returns a string for each parameter set.

@pytest.mark.parametrize("a,b,expected", [ (1, 2, 3), (0, 0, 0), ], ids=["positive", "zero"]) def test_add(a, b, expected): assert a + b == expected

Custom IDs make test reports clearer, especially when you have many cases. They also help when you need to run a specific case with pytest -k 'positive'.

Parametrizing at Class and Module Level

Parametrize can be applied at class or module scope. At module level, assign the mark to pytestmark; decorating a class with @pytest.mark.parametrize follows the same pattern and applies the parameters to every test method in the class.

import pytest pytestmark = pytest.mark.parametrize("base", [10, 20]) class TestMath: def test_double(self, base): assert base * 2 == base + base def test_add_zero(self, base): assert base + 0 == base

This is convenient when a group of tests shares the same setup values. However, be careful: if a test method already has its own parametrize, the two sets combine, producing a cartesian product of cases. That can lead to an explosion in test count if you are not mindful.

Common Pitfalls and Maintainability

Pytest does not copy parameter values before running a test. If you use the same mutable object in multiple parameter sets, a test that mutates it can affect later cases. Prefer immutable values, or create a fresh copy inside the test when you need to mutate the input.

Another issue is readability. When the parameter list becomes long, the test definition gets cluttered. You can extract the data into a module-level list. This separates the test logic from the data and makes it easier to update.

CASES = [ (1, 2, 3), (0, 0, 0), ] @pytest.mark.parametrize("a,b,expected", CASES) def test_add(a, b, expected): assert a + b == expected

Keeping the data close to the test is fine for a few cases, but for larger datasets, consider moving them to a separate module or a JSON file if the data is shared across test files.

Performance and Test Collection Considerations

Parametrized tests are expanded at collection time. That means pytest generates a separate test node for each parameter set before any test runs. For a small number of cases, the overhead is negligible. But if you have thousands of cases, collection time and memory usage increase, and the test report becomes long.

If you need to filter cases at runtime, use pytest.skip inside the test based on a condition, or use pytest.param with marks to skip or mark individual cases as expected to fail:

@pytest.mark.parametrize("value", [ pytest.param(0, marks=pytest.mark.skip(reason='not implemented')), pytest.param(1), ]) def test_value(value): assert value > 0

This allows you to skip individual cases without affecting the others. It also helps when you have known failures that you want to track without breaking the whole test run.

When the number of cases is large, consider whether all cases are necessary. Sometimes a subset with representative edge cases is enough. If you need exhaustive testing, ensure that the test logic is fast and that you are not duplicating setup work that could be shared with fixtures at a higher scope.

Python Parameterized Tests with pytest.mark.parametrize | RYUSLOG DEV