Parametrized Tests in Pytest ( data driven testing )

When we write tests for code, we’re often faced with repeating the same tests multiple times, just with different inputs and expected results. Writing separate tests each time makes our test suites bulky, hard to maintain, and prone to errors. This is where parametrized tests – also known as data-driven testing – come to your rescue.

In this article, we’ll explore parametrized tests in Pytest in detail. We will walk through their significance, implementation, and practical examples that clearly demonstrate how you can leverage this powerful feature in real-world projects.

Let’s discuss them one by one.

Parametrized tests in Pytest enable you to run one test function multiple times with different inputs and expected outputs. Rather than writing separate tests for each combination of parameters, you define a single test and provide Pytest with the sets of inputs you wish to test. Pytest will automatically run the test function for each given scenario.

  • Reduced code redundancy: Write a single test function instead of multiple, very similar tests.
  • Improved readability: Parametrization clearly separates testing logic from test data.
  • Ease of maintenance: Update and maintain test scenarios easily by just editing parameters.

To better understand parametrization, let’s look at an example scenario. Suppose you have a simple function that checks if a given number is even or not:

1
2
3
# simple function to check if number is even
def is_even(num):
    return num % 2 == 0

Now, we want to find out if some numbers are even or not.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# simple function to check if number is even
def is_even(num):
    return num % 2 == 0
 
# test methods
def test_one_even():
    assert is_even(4) == True
 
def test_one_odd():
    assert is_even(5) == False
 
def test_another_even():
    assert is_even(10) == True
 
def test_another_odd():
    assert is_even(11) == False

Once you run the above test file, you’ll notice that all the test cases have been executed.

4 test passed

Everything ran just fine, right? So, what’s the issue?

In the example above, we’ve written the same test multiple times, each with different test data. Now, imagine having to do this for hundreds or even thousands of data points – it would become really hard to manage. To avoid repeating code and keep things clean and efficient, we can make use of parameterization.

Pytest allows you to define parametrized tests using the @pytest.mark.parametrize decorator. Here’s an improved version of the previous tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pytest
from mymodule import is_even
 
@pytest.mark.parametrize(
    "input_number, expected_output",
    [
        (4, True),
        (5, False),
        (10, True),
        (11, False),
    ]
)
def test_is_even(input_number, expected_output):
    assert is_even(input_number) == expected_output

The @pytest.mark.parametrize decorator takes two main parameters:

  • String of comma-separated variable names ("input_number, expected_output"): This includes arguments your test receives.
  • List of tuples: Each tuple serves as a single test scenario, containing input values and expected results.

For instance:

1
2
3
4
5
[
    (2, True),    # checking even number
    (3, False),   # checking odd number
    (-1, False),  # checking negative odd number
]
  • Pytest handles the iteration automatically, making it ideal for data-driven test automation.

Pytest behaves as though you’ve written four separate tests, running test_is_even() individually for each data set provided.

Now, let’s run the test cases using the pytest -v command. The -v flag isn’t mandatory, but it helps by displaying the names of the test cases in the console output.

pytest -v command output

As you can see, the test case was executed four times, each time with a different set of test data.

When you run hundreds of tests, readability matters. You can define custom IDs for each test case scenario explicitly to make each one even more readable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pytest
 
# simple function to check if number is even
def is_even(num):
    return num % 2 == 0
 
@pytest.mark.parametrize(
    "input_number, expected_output",
    [
        (4, True),
        (5, False),
        (10, True),
        (11, False),
    ],
    ids=[
        "Test number 4",
        "Test number 5",
        "Test number 10",
        "Test number 11",
    ]
)
def test_is_even(input_number, expected_output):
    assert is_even(input_number) == expected_output

Output

collected 4 items                                                                                                                                                       

tests/test_1.py::test_is_even[Test number 4] PASSED                                                                                                               [ 25%]
tests/test_1.py::test_is_even[Test number 5] PASSED                                                                                                               [ 50%]
tests/test_1.py::test_is_even[Test number 10] PASSED                                                                                                              [ 75%]
tests/test_1.py::test_is_even[Test number 11] PASSED     
use ids in parameterized test

This helps quickly identify failed test cases during CI/CD runs.

Separating the test cases into variables improves maintainability:

1
2
3
4
5
6
7
8
9
10
11
import pytest
 
# simple function to check if number is even
def is_even(num):
    return num % 2 == 0
 
even_odd_data = [(2, True), (3, False), (0, True), (-1, False)]
 
@pytest.mark.parametrize("number,result", even_odd_data)
def test_is_even(number, result):
    assert is_even(number) == result

Output –

pytest -v for parameterized test where variables are used

Normally, you would use a single parameterized decorator like this:

1
2
3
4
5
6
7
8
9
10
11
12
import pytest
 
# simple function to check if number is even
def is_even(num):
    return num % 2 == 0
 
@pytest.mark.parametrize("number, expected_result", [
    (2, True),
    (3, False),
])
def test_is_even(number, expected_result):
    assert (number % 2 == 0) == expected_result

But what if you have multiple independent parameters, and you want your test to run against all possible combinations of those parameters? This is exactly where stacking comes in handy.

Stacking simply means placing more than one @pytest.mark.parametrize decorator on a test function.

Let’s look at a clear example to understand how stacking works practically.

1
2
3
4
5
6
7
8
import pytest
 
@pytest.mark.parametrize("number", [1, 2, 3])
@pytest.mark.parametrize("multiplier", [10, 100])
def test_multiply(number, multiplier):
    result = number * multiplier
    # Simple check to ensure the multiplication is correct
    assert result == number * multiplier

Pytest generates all possible combinations of parameters listed, running each combination as a separate test.

The above example generates 3 numbers (1, 2, 3) x 2 multipliers (10, 100) = 6 distinct tests:

  • (number=1, multiplier=10)
  • (number=1, multiplier=100)
  • (number=2, multiplier=10)
  • (number=2, multiplier=100)
  • (number=3, multiplier=10)
  • (number=3, multiplier=100)

Let’s run 'pytest -v' to clearly see the output:

Showing stacking of Parameterized Decorators

Notice how Pytest generated and ran every combination automatically.

Suppose you have mistakenly provided fewer or more arguments in the parametrized decorator than your test method expects. Here’s what this mistake might look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pytest
 
# simple function to check if number is even
def is_even(num):
    return num % 2 == 0
 
@pytest.mark.parametrize(
    "number, expected_result"# Two parameters provided
    [
        (2, True, "extra-value"),  # Oops! Three values given instead of two
        (3, False, "extra-value"),
    ]
)
def test_is_even(number, expected_result):
    assert number % 2 == 0 == expected_result

Running this test produces an error

tests/test_1.py::test_is_even: in "parametrize" the number of names (2):
  ['number', 'expected_result']
must be equal to the number of values (3):
  (2, True, 'extra-value')

==============short test summary info =================================
ERROR tests/test_1.py - Failed: tests/test_1.py::test_is_even: in "parametrize" the number of names (2):
error in parameterized test

How to Easily Fix:

Ensure that each parameter set has the exact number of arguments matching the names defined in your decorator:

Another common mistake occurs when you name variables differently between your decorator and test method. Pytest cannot map arguments by position alone – it specifically uses the argument names.

1
2
3
4
5
6
7
8
9
10
11
12
import pytest
 
# parametrization has variables named "num, expected"
@pytest.mark.parametrize(
    "num, expected",
    [
        (4, True),
        (7, False)
    ]
)
def test_is_even(number, expected_result):   # function arguments are different!
    assert (number % 2 == 0) == expected_result

Running this test yields an error:

In test_is_even: function uses no argument 'num'
============================ short test summary info =============================================
ERROR tests/test_1.py - Failed: In test_is_even: function uses no argument 'num'
error because of different variable names

Why?

This happens because the decorator tries to send the arguments "num, expected" to the function, but your function expects "number, expected_result". Pytest doesn’t know you intended these to mean the same thing.

How to Fix:

Simply make sure parameter names match exactly.

This is it. We hope that you have liked the article. If you have any doubts or concerns, please write to us in the comments or mail us at admin@codekru.com.

Related Articles
Liked the article? Share this on

Leave a Comment

Your email address will not be published. Required fields are marked *