pythonprotocolspython-typingmypykeyword-argument

How to define a Python Protocol that is Callable with any number of keyword arguments of Any type?


How do I define a Python protocol for a type that is:

This is my attempt:

from typing import Any, Protocol, TypeVar

T = TypeVar("T", covariant=True)


class Operation(Protocol[T]):
    def __call__(self, **kwargs: Any) -> T:
        pass


# some example functions that should be a structural sub-type of "Operation[str]"
def sumint(*, x: Any, y: Any) -> str:
    return f"{x} + {y} = {x + y}"


def greet(*, name: Any = "World") -> str:
    return f"Hello {name}"


# an example function that takes an "Operation[str]" as an argument
def apply_operation(operation: Operation[str], **kwargs: Any) -> str:
    return operation(**kwargs)


if __name__ == "__main__":
    print(apply_operation(sumint, x=2, y=2))
    # prints: 2 + 2 = 4
    print(apply_operation(greet, name="Stack"))
    # prints: Hello Stack

However, mypy produces the error:

example.py:26: error: Argument 1 to "apply_operation" has incompatible type "Callable[[NamedArg(Any, 'x'), NamedArg(Any, 'y')], str]"; expected "Operation[str]"
example.py:28: error: Argument 1 to "apply_operation" has incompatible type "Callable[[DefaultNamedArg(Any, 'name')], str]"; expected "Operation[str]"
Found 2 errors in 1 file (checked 1 source file)

What am I doing wrong? How do I make mypy happy?


Solution

  • I can't answer your question about exactly why MyPy isn't happy — but here's a different approach that MyPy does seem to be happy with:

    from typing import Any, Callable, TypeVar
    
    T = TypeVar("T", covariant=True)
    
    
    Operation = Callable[..., T]
    
    
    # some example functions that should be a structural sub-type of "Operation[str]"
    def sumint(*, x: int = 1, y: int = 2) -> str:
        return f"{x} + {y} = {x + y}"
    
    
    def greet(*, name: str = "World") -> str:
        return f"Hello {name}"
    
    
    # an example function that takes an "Operation[str]" as an argument
    def apply_operation(operation: Operation[str], **kwargs: Any) -> str:
        return operation(**kwargs)
    
    
    if __name__ == "__main__":
        print(apply_operation(sumint, x=2, y=2))
        # prints: 2 + 2 = 4
        print(apply_operation(greet, name="Stack"))
        # prints: Hello Stack