pytest.mark.parametrize is the secret sauce for turning a single test function into a battery of tests, each running with different inputs and expected outputs.

Let’s see it in action. Imagine you’re testing a simple function that adds two numbers:

# your_module.py
def add(a, b):
    return a + b

Now, you want to test add with various combinations of positive, negative, and zero values. Instead of writing separate test functions like test_add_positive, test_add_negative, etc., you can use parametrize:

# test_your_module.py
import pytest
from your_module import add

@pytest.mark.parametrize("input_a, input_b, expected_output", [
    (1, 2, 3),       # Test case 1: two positive numbers
    (-1, 1, 0),      # Test case 2: one positive, one negative
    (0, 5, 5),       # Test case 3: zero and a positive number
    (-3, -4, -7),    # Test case 4: two negative numbers
    (100, 0, 100),   # Test case 5: a positive number and zero
])
def test_add_various_inputs(input_a, input_b, expected_output):
    assert add(input_a, input_b) == expected_output

When you run pytest, it will execute test_add_various_inputs five times, once for each tuple in the list provided to parametrize. The values within each tuple are unpacked and assigned to the arguments input_a, input_b, and expected_output in the order they appear. The output will show each of these individual test runs, clearly indicating which specific input combination passed or failed.

The core problem parametrize solves is reducing boilerplate and improving test maintainability. Without it, you’d have repetitive test code, making it harder to add new test cases or modify existing ones. Each test function would essentially be a copy-paste job with different literal values. parametrize centralizes your test data, making it a single source of truth.

Internally, pytest uses this decorator to generate multiple test items from a single test function. Each generated item is a distinct test case that pytest discovers and runs independently. This is why you see individual results for each parameterized run in the test output. The first argument to parametrize is a string of comma-separated argument names. These names must exactly match the parameter names in your test function signature. The second argument is an iterable (often a list) of values. Each element in this iterable represents one set of arguments for a single test run.

You can also use parametrize to test different scenarios for a single input, or even to test different functions with the same logic. For instance, if you were testing a subtract function alongside add, you could parametrize the function itself:

import pytest
from your_module import add, subtract

@pytest.mark.parametrize("operation, input_a, input_b, expected_output", [
    (add, 1, 2, 3),
    (subtract, 5, 3, 2),
    (add, -1, 1, 0),
    (subtract, 3, 5, -2),
])
def test_operations(operation, input_a, input_b, expected_output):
    assert operation(input_a, input_b) == expected_output

This demonstrates the flexibility of parametrize – it’s not just for data, but can also inject behavior into your tests.

A common pitfall is forgetting that parametrize generates distinct test items. This means that if one parameterized test fails, pytest will report that specific failure, but it will continue to run the other parameterized instances. You can also combine multiple @pytest.mark.parametrize decorators on a single test function. In this case, pytest will create a Cartesian product of all parameter sets, meaning every combination of the different parameterizations will be tested. For example, if you have one parametrize with 3 values and another with 2 values, your test will run 3 * 2 = 6 times.

The next logical step is to explore how to use pytest.param for more advanced scenarios, like marking specific test cases as expected to fail or skipping them.

Want structured learning?

Take the full Pytest course →