Reputation: 504
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?
Upvotes: 5
Views: 4498
Reputation: 504
Using Callable[..., T]
, as suggested in the other answers, solves the problem in the question.
If one still needs to implement a custom Protocol
(for example if additional methods need to be added to the protocol) this can be done in the following way:
from typing import Any, Protocol, TypeVar
T = TypeVar("T", covariant=True)
class Operation(Protocol[T]):
__call__: Callable[..., T]
See: How to combine a custom protocol with the Callable protocol?
Upvotes: 2
Reputation: 281151
You cannot define a protocol that fits your requirements, because it's fundamentally unsafe from a static typing perspective.
The problem here is that although you say an instance of Operation[T]
should be callable "with any number of keyword arguments of any type", you seem to actually mean that there is some combination of keyword arguments it accepts. It doesn't just accept any keyword arguments.
If you could define an Operation[T]
protocol with the characteristics you want, then your apply_operation
def apply_operation(operation: Operation[str], **kwargs: Any) -> str:
return operation(**kwargs)
would still be a type error. operation(**kwargs)
is unsafe, because there's no guarantee that the provided keyword arguments are arguments operation
accepts. You could call
apply_operation(sumint, name="Stack")
and that would fit the apply_operation
signature, but it would still be an unsafe call.
If you want to annotate this, your best bet is probably to use Callable[..., T]
, as suggested in Alex Waygood's answer. Specifying ...
as the argument type list for Callable
effectively disables type checking for the callable's arguments, much like annotating a variable with Any
effectively disables type checking for that variable.
Remember that this disables the safety checks - there will be no warning if you do something like apply_operation(sumint, name="Stack")
.
Upvotes: 4
Reputation: 7559
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
Upvotes: 3