Description
Bug Report
decorating methods with descriptors loses MyPy when checking against a Protocol. It ends up refusing the assignment because a method isn't Callable[...]
but Descriptor
I wasn't able this exact described behaviour in previous bug reports. It does seem like mypy correctly assess the type of the described class fields and method in other cases.
To Reproduce
from collections.abc import Callable
from typing import Concatenate, Generic, ParamSpec, Protocol, TypeVar
P = ParamSpec("P")
Owner = TypeVar("Owner")
Return = TypeVar("Return")
class Runner(Generic[Owner, P, Return]):
def __init__(self, owner: Owner, func: Callable[Concatenate[Owner, P], Return]):
self.func = func
self.owner = owner
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Return:
return self.func(self.owner, *args, **kwargs)
class MyDescriptor(Generic[Owner, P, Return]):
def __init__(self, func: Callable[Concatenate[Owner, P], Return]):
self.func = func
def __get__(self, owner: Owner, type: type[Owner]) -> Runner[Owner, P, Return]:
return Runner(owner, self.func)
def with_descriptor(func: Callable[Concatenate[Owner, P], Return]) -> MyDescriptor[Owner, P, Return]:
return MyDescriptor(func)
class MyProtocol(Protocol):
def my_method(self, i: int) -> str: ...
class MyClass:
@with_descriptor
def my_method(self, i: int) -> str:
return f"hello {i}"
concrete_instance = MyClass()
reveal_type(concrete_instance.my_method) # W: Type of "concrete_instance.my_method" is "Runner[MyClass, (i: int), str]"
as_callable: Callable[[int], str] = concrete_instance.my_method # successful type checking
my_instance: MyProtocol = concrete_instance # type checking fails
reveal_type(my_instance.my_method) # W: Type of "my_instance.my_method" is "(i: int) -> str"
assert my_instance.my_method(8) == "hello 8"
Expected Behavior
mypy should detect that the __get__
method from the descriptor returns a compatible type for Callable
(it has a compatible __call__
method) and allow the assignment my_instance: MyProtocol = MyClass()
Actual Behavior
mypy correctly detects that the concrete_instance.my_method
type is the one from the return of the __get__
method from the descriptor. It also correctly asses that the type is compatible with Callable[[int], str]
like the method of the protocol.
However, it refuses to verify that this method is indeed compatible with the protocol's method when trying to assign the whole instance to a typed variable.
reproduce.py:42: note: Revealed type is "reproduce.Runner[reproduce.MyClass, [i: builtins.int], builtins.str]"
reproduce.py:45: error: Incompatible types in assignment (expression has type "MyClass", variable has type "MyProtocol") [assignment]
reproduce.py:45: note: Following member(s) of "MyClass" have conflicts:
reproduce.py:45: note: my_method: expected "Callable[[int], str]", got "MyDescriptor[MyClass, [int], str]"
reproduce.py:46: note: Revealed type is "def (i: builtins.int) -> builtins.str"
Found 1 error in 1 file (checked 1 source file)
Your Environment
- Mypy version used: 1.16.1 (compiled: yes)
- Mypy command-line flags: none.
- Mypy configuration options from
mypy.ini
(and other config files):check_untyped_def=True
- Python version used: 3.12.3