Search code examples
pythonpytestpython-typingmypy

How to make work typing annotation on mypy with unpacking params for pytest parameterized test?


I have a simple method on my_module.py with the signature:

def my_method(value: float, extra: str = "something") -> str:
    pass

Then I have a parameterized test on test_my_module.py for it:

from typing import List
from typing import Union

import pytest

import my_module

@pytest.mark.parametrize(
    "params, expected",
    [
        ([1.0], "result1"),
        ([2.0], "result2"),
        ([2.0, "extra"], "result3")
    ],
)
def test_my_method(params: List[Union[int, str]], expected: str) -> None:
    assert my_module.my_method(*params) == expected

When I run dynamic type tests such as typeguard it passes. But with mypy I get these [arg-type] errors:

tests/test_my_module.py: note: In function "test_my_method":
tests/test_my_module.py:50:30: error: Argument 1 to "my_method" has incompatible type "*List[Union[int, str]]"; expected "float" 
[arg-type]
        assert my_module.my_method(*params) == expected
                                   ^
tests/test_my_module.py:50:30: error: Argument 1 to "my_method" has incompatible type "*List[Union[int, str]]"; expected "str" 
[arg-type]
        assert my_module.my_method(*params) == expected
                                   ^

Not really sure how should I annotate these types to make it pass. Note that this is a minimal example, so I want to stick unpacking the list of arguments params in the list I create to parameterize the tests.

Any thoughts?


Solution

  • I'd recommend representing your variable arguments as a tuple instead of a list. typing does not currently allow for lists to have specific types at specific indicies, however a fixed-size tuple can specify types for its dimensions

    The two shapes you have currently are either a single float: Tuple[float] or a float and a string: Tuple[float, str] -- by converting your parametrize call and arguments to this it should work nicely:

    from typing import Tuple
    from typing import Union
    
    import pytest
    
    import my_module
    
    @pytest.mark.parametrize(
        "params, expected",
        [
            ((1.0,), "result1"),
            ((2.0,), "result2"),
            ((2.0, "extra"), "result3")
        ],
    )
    def test_my_method(params: Union[Tuple[float], Tuple[float, str]], expected: str) -> None:
        assert my_module.my_method(*params) == expected
    

    it may also make sense to split up your tests -- you might be over-using parametrize here and it may be more clear to have one set of tests for your fixed arguments and another set of tests for your optional argument (instead of trying to jam all of those tests together with parametrize) -- perhaps something like this:

    @pytest.mark.parametrize(
        "arg, expected",
        [
            (1.0, "result1"),
            (2.0, "result2"),
        ],
    )
    def test_my_method_one_arg(arg: float, expected: str) -> None:
        assert my_module.my_method(arg) == expected
    
    
    @pytest.mark.parametrize(
        "arg, extra, expected",
        [
            (1.0, "extra", "result3"),
            (2.0, "extra", "result4"),
        ],
    )
    def test_my_method_extra_arg(arg: float, extra: str, expected: str) -> None:
        assert my_module.my_method(arg, extra) == expected