Skip to main content

Dictionary-based test parametrization for pytest

The decorator @d_parametrize (defined in pytest_dparam) offers an arguably more readable alternative to @pytest.mark.parametrize.

Test cases are configured through a dictionary so that:

  • The name for each test case precedes its definition (the list of values to be used),
  • The name for each argument/value is repeated in the test case definition.

Additionally, test cases covering the same topic can be bundled under the same name.

Install

pip install pytest-dparam

Syntax

d_parametrize takes as its input a dictionary, whose entries are the different test cases or sets of test cases (named through the dictionary keys).

Every test case is defined with a dictionary of value assignments.

For example:

from pytest_dparam import d_parametrize

def square(x: int) -> int:
    return x * x

@d_parametrize(
    {
        "trivial_case": {"input": 1, "expected": 1},  # test_square[trivial_case]
        "negative_trivial_case": [
            {"input": -1, "expected": 1},  # test_square[negative_trivial_case]
        ],
        "positive_integers": [
            {"input": 2, "expected": 4},  # test_square[positive_integers_0]
            {"input": 3, "expected": 9},  # test_square[positive_integers_1]
        ],
        "negative_integers": [
            {"input": -2, "expected": 4},  # test_square[negative_integers_0]
            {"input": -3, "expected": 9},  # test_square[negative_integers_1]
        ],
    }
)
def test_square(input: int, expected: int):
    assert square(input) == expected

The keys are the names given to each test case or set of test cases.

Defining an isolated test case

Each test case is described by a dictionary where the keys are the name of the arguments to be defined (which would be the first argument of pytest.mark.parametrize), and the values are the values to be given to said arguments.

For example, to ensure that our square function returns 1 for both 1 and -1:

@d_parametrize(
    {
        "trivial_case": {"input": 1, "expected": 1},
        "negative_trivial_case": {"input": -1, "expected": 1},
    }
)
def test_square(input: int, expected: int):
    assert square(input) == expected

⚠ All test cases must include the same keys in the same order. Otherwise, exception pytest_dparam.InvalidParametrizedArgument will be raised.

Bundling cases together

It can be useful to provide several test cases to cover similar situations, calling for a same name. In such a case, a list of test-case-describing dictionaries (or actually, any iterable of such dictionaries) can be provided instead of a single dictionary.

For example, if we want to test square with different negative numbers just to be sure:

@d_parametrize(
    {
        # ...
        "negative_integers": [
            {"input": -2, "expected": 4},
            {"input": -3, "expected": 9},
        ],
    }
)
def test_square(input: int, expected: int):
    assert square(input) == expected

The name given to the set of test cases will be used for each included test case, with a numbered suffix (e.g., negative_integers_0 and negative_integers_1 in the previous example).

Pseudo-bundling isolated cases for readability

For readability, you might appreciate having one line for the test case name, followed by a single-line test case description. However, a code formatter such as Black can get in the way.

Or you might appreciate using lists all the time for consistency.

In any case, you can put an isolated test cases within a list. If it is alone in the list, its name will not be affected:

@d_parametrize(
    {
        "trivial_case": [
            {"input": 1, "expected": 1},
        ],
        "negative_trivial_case": [
            {"input": -1, "expected": 1},
        ],
    }
)
def test_square(input: int, expected: int):
    assert square(input) == expected

With test classes, mocks, etc.

The test function might be required to use additional arguments to the parametrized ones, such as a reference to self when in a test class, or monkeypatch: pytest.MonkeyPatch for mocking. As when using pytest.mark.parametrize, those are simply ignored when using d_parametrize:

class Test_class:
    @d_parametrize(
        {
            "trivial_case": [
                {"input": 1, "expected": 1},
            ],
            # ...
        }
    )
    def test_fun(input: int, expected: int, monkeypatch: pytest.MonkeyPatch):
        # ...
        assert actual == expected

Under the hood

d_parametrize(…) actually just calls pytest.mark.parametrize(…) with the proper arguments, based on the provided parametrization-describing dictionary, and after asserting that the included test cases are compatible and valid.

Ultimately:

@d_parametrize(
    {
        "trivial_case": {"input": 1, "expected": 1},  # test_square[trivial_case]
        "negative_trivial_case": [
            {"input": -1, "expected": 1},  # test_square[negative_trivial_case]
        ],
        "positive_integers": [
            {"input": 2, "expected": 4},  # test_square[positive_integers_0]
            {"input": 3, "expected": 9},  # test_square[positive_integers_1]
        ],
        "negative_integers": [
            {"input": -2, "expected": 4},  # test_square[negative_integers_0]
            {"input": -3, "expected": 9},  # test_square[negative_integers_1]
        ],
    }
)
def test_square(input: int, expected: int):
    assert square(input) == expected

is literally equivalent to:

@pytest.mark.parametrize(
    ("input", "expected"),
    [
        pytest.mark.parametrize( 1, 1, id="trivial_case"),
        pytest.mark.parametrize(-1, 1, id="negative_trivial_case"),
        pytest.mark.parametrize( 2, 4, id="positive_integers_"),
        pytest.mark.parametrize( 3, 9, id="positive_integers_"),
        pytest.mark.parametrize(-2, 4, id="negative_integers_"),
        pytest.mark.parametrize(-3, 9, id="negative_integers_"),
    ]
)
def test_square(input: int, expected: int):
    assert square(input) == expected

Release files for pytest-dparam 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pytest-dparam 1.0.0
File Size Uploaded
pytest_dparam-1.0.0.tar.gz 5.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pytest-dparam 1.0.0
File Interpreter ABI Platform
pytest_dparam-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 10.5 kB

Release files / pytest_dparam-1.0.0.tar.gz

Download URL pytest_dparam-1.0.0.tar.gz
Size 5.8 kB
Tags Source
SHA-256 checksum
How to use checksums
7ae08dc26c3ddb94301aebba0b8e876423efcb027cfd6c81f7748059f00e794b
BLAKE2b-256 checksum
How to use checksums
a3e3f864222dab87c72b1304bb065df9de42cdb6962df50eb42190eb6ea8ad0f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/5.1.1 CPython/3.12.3

Release files / pytest_dparam-1.0.0-py3-none-any.whl

Download URL pytest_dparam-1.0.0-py3-none-any.whl
Size 4.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ef0695e091caa5053e925bf09e7995611fca75d5a94b5e9d36c7f1b538703899
BLAKE2b-256 checksum
How to use checksums
bb8edd161d90f672a5a2becb5f1f369588ebc431661b4438210e72a3dff131a0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/5.1.1 CPython/3.12.3

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page