Back to Blog
Python

Python pytest parametrize and parameterized tests

python pytest parametrize and parameterized tests: Learn how to use pytest.mark.parametrize to create parameterized tests in Python, covering syntax, fixtures, IDs, an...

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. This is the core of python pytest parametrize and parameterized tests.

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 order of the names in the decorator string must match the order of values in each tuple. If you later change the order, the test will fail because the arguments are bound positionally.

Using Parametrize with Fixtures

Parametrize works alongside fixtures. You can pass fixture values as arguments to a parametrized test, but the more powerful pattern is indirect parametrization. With indirect=True, the argument names are treated as fixture names, and the values are passed to those fixtures. This allows you to 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 takes the parameter set and returns a string.

@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

You can apply parametrize to a class or to a module by using the pytestmark variable. When applied to a class, every test method in that class receives the parameters. When applied at module level, all tests in the module do.

import pytest pytestmark = pytest.mark.parametrize("base", [10, 20]) class TestMath: def test_mul(self, base): assert base * 2 == base * 2 def test_add(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

One common mistake is using mutable objects as parameters, such as lists or dictionaries. If the test modifies the parameter, the modification persists across cases because the same object is reused. To avoid this, use immutable values or create a fresh copy inside the test.

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 or a fixture that returns the 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.

To manage large parameter sets, you can use pytest.mark.parametrize with a generator or a fixture that yields values lazily. However, note that the entire list must be available at collection time, so a generator will be consumed. 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 mark specific cases as skipped or 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 pytest parametrize and parameterized tests: Practical | RYUSLOG DEV