The typing spec is currently unclear when it comes to the difference between the following two protocol definitions:
class Proto1[T](Protocol):
def __call__(self, arg: T, /) -> T: ...
class Proto2(Protocol):
def __call__[T](self, arg: T, /) -> T: ...
The semantics of Proto1
is well-documented already. However, the semantics of Proto2
isn’t. Hence my question: should such a definition be allowed and if so, how should it be interpreted?
In my opinion, a natural interpretation for Proto2
is to describe a polymorphic function of one argument. Technically, allowing such an interpretation introduces some form of rank-2 polymorphism into the language.
For example:
from typing import Protocol
class PolyIdentity(Protocol):
def __call__[T](self, arg: T, /) -> T: ...
def apply_twice(f: PolyIdentity, /) -> tuple[int, str]:
return f(1), f("a")
def id[T](x: T) -> T:
return x
def incr(x: int) -> int:
return x + 1
apply_twice(id) # should be accepted
apply_twice(incr) # should error
Version without PEP 695 features, to test with mypy
from typing import Protocol, TypeVar
T = TypeVar("T")
class PolyIdentity(Protocol):
def __call__(self, arg: T, /) -> T: ...
def apply_twice(f: PolyIdentity, /) -> tuple[int, str]:
return f(1), f("a")
def id(x: T) -> T:
return x
def incr(x: int) -> int:
return x + 1
apply_twice(id)
apply_twice(incr) # mypy error
Here, the id
function implements the PolyIdentity
protocol but the incr
function does not, since it only accepts integer inputs.
As of today, mypy 1.11.1 is acting as I would expect on the snippet above, rejecting the second call to apply_twice
but not the first one. Pyright 1.1.375 accepts both calls.
It would be good for the typing spec to decide on the correct behaviour here and document it.
Note: This post is inspired from a pyright issue.