Why doesn't this method register as an instance of the Protocol?

I’ve got a ‘problem’ in my code where mypy doesn’t register that I’ve typed things correctly, even though I think I have.

I’ve pared this down to a somewhat minimal example, which is below.
The following mypy does recognise things are correct:

import asyncio
from typing import Protocol


class P(Protocol):
    async def __call__(self, a: int, b: int) -> bool: ...


class C:
    async def m(self, a: int, b: int) -> bool:
        return a < b


async def f(p: P) -> bool:
    ans = await p(a=1, b=2)
    return ans


async def g():
    c = C()
    ans = await f(c.m)
    print(ans)


asyncio.run(g())

and here it does not:

from datetime import datetime
from typing import Protocol

from attrs import frozen


@frozen
class MyClass:
    async def __aenter__(self) -> "MyClass":
        return self

    async def __aexit__(
        self,
        _exc_type: type[BaseException] | None,
        _exc_value: BaseException | None,
        _traceback: object,
    ) -> None:
        pass

    @classmethod
    async def from_yaml(
        cls,
    ) -> "MyClass":
        return cls()

    async def get_internal_trades(
        self,
        start_execution_time: datetime | None = None,
        offset: int = 0,
        page_size: int = 150,
    ) -> list["dict"]:
        return []


class InternalTradesGetter(Protocol):
    """Type stub to represent a get_internal_trades function."""

    async def __call__(
        self,
        start_execution_time: datetime | None,
        offset: int,
        limit: int,
    ) -> list[dict]: ...


async def fun(trades_getter: InternalTradesGetter) -> None:
    return None


async def main() -> None:
    async with await MyClass.from_yaml() as pbc:
        await fun(pbc.get_internal_trades)

Does anyone recognise what’s going on?
Is there a Python typing rule I missed?

The mypy error message is

Argument 1 to "fun" has incompatible type "Callable[[datetime | None, int, int], Coroutine[Any, Any, list[dict[Any, Any]]]]"; expected "InternalTradesGetter"
"InternalTradesGetter.__call__" has type "Callable[[Arg(datetime | None, 'start_execution_time'), Arg(int, 'offset'), Arg(int, 'limit')], Coroutine[Any, Any, list[dict[Any, Any]]]]"

so it seems that something here is causing information about the argument names to get discarded.

In MyClass get_internal_trades takes page_size, but in the protocol it’s limit. If you called with keyword arguments, that’d fail. You can fix this either by making the arguments the same, or making the protocol positional only with /. Then you can’t call it with keyword args, so the names don’t matter.

The error message probably should show keyword argument names in both, probably a bug there.

1 Like