pythonpython-typingmypy

Why is a type identified as not itself in mypy?


MRE: https://mypy-play.net/?mypy=latest&python=3.10&gist=263dfa4914d0317291638957e51e8700&flags=strict

from typing import Callable, TypeVar, Type, Protocol

from typing_extensions import Self

_CT = TypeVar("_CT")
_T = TypeVar("_T")


class LtConverter(Protocol):
    def __new__(cls: Type[_T], x: _CT) -> _T: ...

    def __lt__(self, other: Self) -> bool: ...


def lt_through(conv: Type[LtConverter]) -> Callable[[_CT, _CT], bool]:
    def comparator(op: Callable[[LtConverter, LtConverter], bool]) -> Callable[[_CT, _CT], bool]:
        def method(self: _CT, other: _CT) -> bool:
            return op(conv(self), conv(other))
        return method
    return comparator(lambda x, y: x < y)

In the MRE, lt_through(conv) returns a function, which can be used as a method for a class, such that the "less than" operator of the class compares the class after been casted to type conv, which is any class supporting the protocol to casting and the less than operation with itself.

mypy gives the following error message:

main.py:18: error: Argument 1 to "LtConverter" has incompatible type "_CT"; expected "_CT"  [arg-type]

I am vaguely confused by this message. Why is _CT considered to be different from _CT? Why does this error occur for conv(self) but not for conv(other) (which can be seen by inspecting the char number in the verbose output)?


Solution

  • Your code is almost correct - mypy's reporting just didn't do very well to allow easily fixing the remaining issues. I'll answer these two first:


    If I understand your intention correctly, to fix this, you just have to change LtConverter.__new__ to one of the following:

    This is based on the assumptions that

    In practice, your intended implementation might be a bit stricter than this, in which case you should consider putting a type variable bound on _CT, parameterising LtConverter with a _CT type, and/or replacing LtConverter.__new__::x: typing.Any with x: _CT. Here's one possible version with stricter typing:

    from typing import Callable, TypeVar, Type, Protocol
    
    from typing_extensions import Self
    
    _CT = TypeVar("_CT")
    _T = TypeVar("_T")
    _ConverteeT = TypeVar("_ConverteeT", covariant=True)
    
    
    class LtConverter(Protocol[_ConverteeT]):
        def __new__(cls: Type[_T], x: _ConverteeT) -> _T: ...
    
        def __lt__(self, other: Self) -> bool: ...
    
    
    def lt_through(conv: Type[LtConverter[_CT]]) -> Callable[[_CT, _CT], bool]:
        def comparator(op: Callable[[LtConverter[_CT], LtConverter[_CT]], bool]) -> Callable[[_CT, _CT], bool]:
            def method(self: _CT, other: _CT) -> bool:
                return op(conv(self), conv(other))
            return method
        return comparator(lambda x, y: x < y)