Skip to content

False positive with builtins.min(T, T) if T.__lt__ doesn't return a builtins.bool #12562

Description

@jorenham

Since NumPy 2.1 all subtypes of numpy.number implement __lt__ that returns a numpy.bool (which isn't a builtins.bool), matching the runtime behavior (numpy/numpy#26942).

But as was noticed in numpy/numpy#27251, the consequence of this is that builtins.min with e.g. numpy.int32 input is now rejected (tested with the latest mypy and pyright).

So at runtime we have

>>> import numpy as np
>>> np.__version__
'2.1.0'
>>> np.int32(8) < np.int32(9)
np.True_
>>> min(np.int32(8), np.int32(9))
np.int32(8)

But mypy 1.11.1 rejects min(np.int32(8), np.int32(9)) as:

error: No overload variant of "min" matches argument types "signedinteger[_32Bit]", "signedinteger[_32Bit]"  [call-overload]

Similarly, Pyright 1.1.376 reports

error: Argument of type "int32" cannot be assigned to parameter "arg1" of type "SupportsRichComparisonT@min" in function "min"
    Type "int32" is incompatible with type "SupportsRichComparison"
      Type "int32" is incompatible with type "SupportsRichComparison"
        "signedinteger[_32Bit]" is incompatible with protocol "SupportsDunderLT[Any]"
          "__lt__" is an incompatible type
            No overloaded function matches type "(other: _T_contra@SupportsDunderLT, /) -> bool"
        "signedinteger[_32Bit]" is incompatible with protocol "SupportsDunderGT[Any]"
          "__gt__" is an incompatible type
            No overloaded function matches type "(other: _T_contra@SupportsDunderGT, /) -> bool" (reportArgumentType)

and

error: Argument of type "int32" cannot be assigned to parameter "arg2" of type "SupportsRichComparisonT@min" in function "min"
    Type "int32" is incompatible with type "SupportsRichComparison"
      Type "int32" is incompatible with type "SupportsRichComparison"
        "signedinteger[_32Bit]" is incompatible with protocol "SupportsDunderLT[Any]"
          "__lt__" is an incompatible type
            No overloaded function matches type "(other: _T_contra@SupportsDunderLT, /) -> bool"
        "signedinteger[_32Bit]" is incompatible with protocol "SupportsDunderGT[Any]"
          "__gt__" is an incompatible type
            No overloaded function matches type "(other: _T_contra@SupportsDunderGT, /) -> bool" (reportArgumentType)

Activity

  1. changed the title [-]False negative with `builtins.min(T, T)` if `T.__lt__` doesn't return a `builtins.bool`[/-] [+]False positive with `builtins.min(T, T)` if `T.__lt__` doesn't return a `builtins.bool`[/+] on Aug 20, 2024
  2. srittau commented on Aug 21, 2024

    @srittau
    Collaborator

    I think this ultimately boils down to the definition of SupportsDunderLT etc:

    class SupportsDunderLT(Protocol[_T_contra]):
    def __lt__(self, other: _T_contra, /) -> bool: ...
    class SupportsDunderGT(Protocol[_T_contra]):
    def __gt__(self, other: _T_contra, /) -> bool: ...
    class SupportsDunderLE(Protocol[_T_contra]):
    def __le__(self, other: _T_contra, /) -> bool: ...
    class SupportsDunderGE(Protocol[_T_contra]):
    def __ge__(self, other: _T_contra, /) -> bool: ...

    These expect the dunders to return a real bool. I'm not sure how np.bool is implemented, so this may be hard to fix.

  3. ngoldbaum commented on Aug 21, 2024

    @ngoldbaum

    Unfortunately bool isn't subclassable, so np.bool is its own separate thing. It's backed by 0 and 1 8-bit int values.

  4. randolf-scholz commented on Aug 21, 2024

    @randolf-scholz
    Contributor

    This should be solvable by returning SupportsBool where

    class SupportsBool(Protocol):
        def __bool__(self) -> bool: ...
  5. JelleZijlstra commented on Aug 21, 2024

    @JelleZijlstra
    Member

    That protocol is equivalent to object in practice, so we'd be better off simply using object as the return type in the SupportDunder* protocols.

  6. randolf-scholz commented on Aug 21, 2024

    @randolf-scholz
    Contributor

    @JelleZijlstra At runtime yes, but currently the type hints for object do not include __bool__. Hence, it makes a difference at type checking time: Code sample in pyright playground

    from typing import Protocol
    
    class SupportsBool(Protocol):
        def __bool__(self) -> bool: ...
    
    x: SupportsBool = object()  # raises reportAssignmentType

    I'm not sure why __bool__ is not declared on object, but there is probably some reason for it.
    EDIT: It's because object does not have __bool__ at runtime.

  7. randolf-scholz commented on Aug 21, 2024

    @randolf-scholz
    Contributor

    OK object seems the right thing as the following works at runtime:

    class Foo:
        def __lt__(self, other): return self
        def __gt__(self, other): return self
        def __le__(self, other): return self
        def __ge__(self, other): return self
    
    x = Foo()
    y = Foo()
    assert min(x, y) is y  # ✅
    x.__bool__  # ❌ AttributeError
  8. randolf-scholz commented on Aug 21, 2024

    @randolf-scholz
    Contributor

    There is also a very similar definition in _operator.pyi, with this chance these maybe can be combined

    class _SupportsDunderLT(Protocol):
    def __lt__(self, other: Any, /) -> Any: ...
    class _SupportsDunderGT(Protocol):
    def __gt__(self, other: Any, /) -> Any: ...
    class _SupportsDunderLE(Protocol):
    def __le__(self, other: Any, /) -> Any: ...
    class _SupportsDunderGE(Protocol):
    def __ge__(self, other: Any, /) -> Any: ...
    _SupportsComparison: TypeAlias = _SupportsDunderLE | _SupportsDunderGE | _SupportsDunderGT | _SupportsDunderLT

  9. randolf-scholz commented on Aug 21, 2024

    @randolf-scholz
    Contributor

    PR #12573 replaces these bool return hints with object. mypy-primer looks good.

    However, it makes me wonder whether the bool return hint should be replaced by object in other cases as well, such as SupportsGetItem.__contains__ and similar protocols.

  10. srittau commented on Jul 7, 2025

    @srittau
    Collaborator

    I don't think we'll get a consensus here. Closing this in favor of the status quo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions