Skip to content

method descriptors aren't properly checked when assinging to a Protocol #19336

Closed
@Floby

Description

@Floby

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"

mypy playground

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrong

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions