Search code examples
pythonpytestmypypython-typing

Callable pytest fixture with keyword argument causes mypy linting error


I have following setup:

@pytest.fixture
def check_answer() -> t.Callable[[dict[str, t.Any], dict[str, t.Any], int], None]:
    def _check(
        first_response: dict[str, t.Any],
        second_response: dict[str, t.Any],
        named_optional_arg: int = 1,
    ) -> None:
        
        *assert stuff*

    return _check

I then pass check_answer as a parameter to test_my_answer and I want to call it as check_answer(ans1, ans2, named_optional_arg=5). The optional argument is used very rarely, so it is important that the name is present when it is called, however, when I include the name, I get an error from mypy because of an unexpected keyword argument.


Solution

  • The problem is that your type signature is not specific enough to capture the fact that _check has named_optional_arg. Callables are not sufficient to type keyword arguments, that is where callback protocols must be used:

    from collections.abc import Mapping
    from typing import Protocol, Any
    
    class Checker(Protocol):
        def __call__(
            self,
            first_response: Mapping[str, Any],
            second_response: Mapping[str, Any],
            /,
            *,
            named_optional_arg: int = ...
        ):
            ...
    

    You can read What do * (single star) and / (slash) do as independent parameters? for more information on how / and * can be used within the parameter list to describe the exact way the function is allowed to be invoked. In this situation, I've made it so the first two arguments must be specified by position, and the third optional one must be specified by name.

    Additionally, note that I replaced dict with Mapping. This is because checking / asserting requires only reading from, not writing to the responses.

    You would then change your declaration to:

    @pytest.fixture
    def check_answer() -> Checker:
        ...