6

I would like to annotate both parameters of a function – supposed to perform simple comparison between them ­– in order to indicate that their types should be the same and should support simple equality comparison.

The second part should be normally trivial, as every type implicitly inherits from object which promises to support this:

class object:

    def __eq__(self, value: object, /) -> bool: ...

and yet numpy.ndarray does not, because its parent class overrides the method as

class _ArrayOrScalarCommon:

    def __eq__(self, other: Any) -> Any: ...

in order to return arrays of booleans with the shape of the argument instead of a unique boolean. This should already be marked as incompatible override as it represents a violation of the LSP. Nevertheless, I want to enforce correctness of the function at type level so that users know that it should not be used with two arguments of type numpy.ndarray or of different types.

Simple protocol annotation does not work:

from typing import Protocol
from numpy import array


class SupportsSimpleComparison(Protocol):

    def __eq__(self, other: object, /) -> bool: ...


def check_equal(a: SupportsSimpleComparison, b: SupportsSimpleComparison) -> bool:
    return a == b

# no errors reported from pyright
check_equal(array([0, 0, 0]), array([0, 0, 1]))
check_equal(1.0, "hello world")

Nor does making the function generic with a type variable bound to the protocol:

T = TypeVar("T", bound=SupportsSimpleComparison)


def check_equal(a: T, b: T) -> bool:
    return a == b


# no errors reported from pyright
check_equal(array([0, 0, 0]), array([0, 0, 1]))
check_equal(1.0, "hello world")
1
  • 1
    ndarray's choice to use Any as the return type fundamentally disables the type check. In general you are allowed to pass an Any as a bool, it would be very frustrating if you couldn't. Without updating numpy, I don't know if there is a solution. You might need to fall back to runtime validation. Commented Sep 16 at 10:04

2 Answers 2

3

I'm unsure of your problem scope, but I believe that can just do this (see mypy and pyright):

from typing_extensions import Protocol, TypeAlias, deprecated, overload

array: TypeAlias = list[int]

class ExactType[T](Protocol):
    @property  # type: ignore
    def __class__(self, /) -> type[T]: ... 
    @__class__.setter
    def __class__(self, Class: type[T], /) -> None: ...

@overload
@deprecated("arrays are not supported")
def check_equal(a: array, b: array) -> bool: ...
@overload
def check_equal[T](a: ExactType[T], b: T) -> bool: ...
def check_equal[T](a: ExactType[T], b: T) -> bool:
    return a == b
from typing import TypeAlias
array: TypeAlias = list[int]

check_equal(array([0, 0, 0]), array([0, 0, 1]))  # Error (Deprecated)
check_equal(1.0, "hello world")                  # Error
check_equal(1, 2)                                # OK
check_equal("hello", "world")                    # OK

The second overload of check_equal works because ExactType[T] forces an invariant solution for T to be solved first when check_equal receives an argument for a.

Sign up to request clarification or add additional context in comments.

1 Comment

This is very promising, I have tried adapting it to python 3.10. It correctly marks two errors (reportDeprecated and reportArgumentType)
3

There is a way to make it works, but it relies on a not-very-popular trick1 that might be (formally) removed from the type system: Any is assignable to Never.

Never is the bottom type, which means the only (fully static) subtype of it is itself. However, Any is also assignable to Never, being a non-fully-static type that can materialize to Never.


That means we can define a protocol that only types with "weird" __bool__ will match:

class HasWeirdDunderBoolOverride(Protocol):
    def __bool__(self, other: Any, /) -> Never: ...

...then use it in a deprecated overload:

(playground)

@overload
@deprecated('Objects have weird __bool__ overrides')
def check_equal(a: HasWeirdDunderBoolOverride, b: HasWeirdDunderBoolOverride) -> Any: ...
@overload
def check_equal(a: SupportsSimpleComparison, b: SupportsSimpleComparison) -> bool: ...

def check_equal(a: object, b: object) -> bool:
    return a == b
check_equal(array([0, 0, 0]), array([0, 0, 1]))  # error: Objects have weird __bool__ overrides
check_equal(1.0, 'hello world')                  # fine

1: The author of that post, Joren Hammudoglu, is a Numpy maintainer.

5 Comments

Interesting, even though not what I was looking for, as it implies knowing the signature I want to reject, as opposed to enforcing the one I need. I should probably have mentioned that I needed my function to work with python 3.10.
@Vexx23 You can either use NoReturn (added in 3.6.2) instead of Never (added in 3.11), or import Never from typing_extension.
Also, in which case does this not work?
This check_equal(1.0, 'hello world')
1.0 == 'hello world' isn't an error per se (consider also indirect calls such as def f(a: object, b: object): return check_equal(a, b) / f(1.0, 'hello world')), but, if you want to, you can combine this approach with @dROOOze's.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.