microsoft / microsoft/pyright

Order dependent inference results with some protocols against numpy

Open
#10,607 0 comments 2 reactions 0 assignees View on GitHub
bug
Dominant language
Python
Stars
15.6k
Forks
1.8k
Avg merge
12h 13m
Merged PRs (30d)
52

Description

**Describe the bug**

I discovered a very curious behavior when checking some `Protocol`-types supposed to abstract numpy arrays. We define 3 Protocols `IntegerArray`, `FloatArray` and `ComplexArray`. Then running pyright on

```python
_numpy_complex: ComplexArray = np.array([1 + 1j], dtype=np.complex128)
_numpy_float: FloatArray = np.array([1.0], dtype=np.float64)
_numpy_int: IntegerArray = np.array([1], dtype=np.int64)
```

yields 0 issues, but checking them in reverse order

```python
_numpy_int: IntegerArray = np.array([1], dtype=np.int64)
_numpy_float: FloatArray = np.array([1.0], dtype=np.float64)
_numpy_complex: ComplexArray = np.array([1 + 1j], dtype=np.complex128)
```

yields 2 issues.

```
tmp.py:32:28 - error: Type "NDArray[signedinteger[_64Bit]]" is not assignable to declared type "IntegerArray[Unknown]"
  "ndarray[_AnyShape, dtype[signedinteger[_64Bit]]]" is incompatible with protocol "IntegerArray[Unknown]"
    Could not bind method "__add__" because "ndarray[_AnyShape, dtype[signedinteger[_64Bit]]]" is not assignable to parameter "self"
      "ndarray[_AnyShape, dtype[signedinteger[_64Bit]]]" is not assignable to "ndarray[_AnyShape, dtype[numpy.bool[builtins.bool]]]"
        Type parameter "_DTypeT_co@ndarray" is covariant, but "dtype[signedinteger[_64Bit]]" is not a subtype of "dtype[numpy.bool[builtins.bool]]"
          "dtype[signedinteger[_64Bit]]" is not assignable to "dtype[numpy.bool[builtins.bool]]"
    Could not bind method "__add__" because "ndarray[_AnyShape, dtype[signedinteger[_64Bit]]]" is not assignable to parameter "self"
      "ndarray[_AnyShape, dtype[signedinteger[_64Bit]]]" is not assignable to "ndarray[_AnyShape, dtype[numpy.bool[builtins.bool]]]"
        Type parameter "_DTypeT_co@ndarray" is covariant, but "dtype[signedinteger[_64Bit]]" is not a subtype of "dtype[numpy.bool[builtins.bool]]"
... (reportAssignmentType)

tmp.py:34:32 - error: Type "NDArray[complex128]" is not assignable to declared type "ComplexArray[Unknown]"
  "ndarray[_AnyShape, dtype[complex128]]" is incompatible with protocol "ComplexArray[Unknown]"
    Could not bind method "__abs__" because "ndarray[_AnyShape, dtype[complex128]]" is not assignable to parameter "self"
      Type "ndarray[_AnyShape, dtype[complex128]]" is not assignable to type "NDArray[floating[Any] | integer[Any] | timedelta64[timedelta | int | None] | numpy.bool[builtins.bool] | object_]"
        "ndarray[_AnyShape, dtype[complex128]]" is not assignable to "ndarray[_AnyShape, dtype[floating[Any] | integer[Any] | timedelta64[timedelta | int | None] | numpy.bool[builtins.bool] | object_]]"
          Type parameter "_DTypeT_co@ndarray" is covariant, but "dtype[complex128]" is not a subtype of "dtype[floating[Any] | integer[Any] | timedelta64[timedelta | int | None] | numpy.bool[builtins.bool] | object_]"
    "__abs__" is an incompatible type
      Type "() -> ndarray[_AnyShape, dtype[floating[_64Bit]]]" is not assignable to type "() -> FloatArray[Unknown]"
        Function return type "ndarray[_AnyShape, dtype[floating[_64Bit]]]" is incompatible with type "FloatArray[Unknown]"
... (reportAssignmentType)
```

Since these statements are independent, I would expect that different orders give the same type inference. I guess something goes wrong with caching subtyping relationships of `np.ndarray` against those protocols. The `__add__` overloads (which are likely defective due to `float=float|int`) seem to play into this somehow.

**Code or Screenshots**

I reduced the protocols as much as possible so that I could still observe the behavior.

```python
from typing import Protocol, Self, overload
import numpy as np # version 2.3.0

class IntegerArray[T](Protocol):
@overload
def __add__(self, other: float, /) -> "FloatArray": ...
@overload
def __add__(self, other: Self | T | int, /) -> Self: ...
@overload
def __radd__(self, other: float, /) -> "FloatArray": ...
@overload
def __radd__(self, other: Self | T | int, /) -> Self: ...

class FloatArray[T](Protocol):
def __floordiv__(self, other: Self | T | float, /) -> Self: ...
def __rfloordiv__(self, other: Self | T | float, /) -> Self: ...

class ComplexArray[T](Protocol):
def __abs__(self) -> FloatArray: ...

# checking in this order -> no issues reported
# _numpy_complex: ComplexArray = np.array([1 + 1j], dtype=np.complex128)
# _numpy_float: FloatArray = np.array([1.0], dtype=np.float64)
# _numpy_int: IntegerArray = np.array([1], dtype=np.int64)

# checking in this order -> 2 issues reported
_numpy_int: IntegerArray = np.array([1], dtype=np.int64)
_numpy_float: FloatArray = np.array([1.0], dtype=np.float64)
_numpy_complex: ComplexArray = np.array([1 + 1j], dtype=np.complex128)
```

If your code relies on symbols that are imported from a third-party library, include the associated import statements and specify which versions of those libraries you have installed.

**VS Code extension or command-line**

pyright version 1.1.402 (python wrapper from pypi)

Contributor guide

Open the contributing guide

Research direction

Reproduce the issue by running pyright 1.1.402 on the reduced tmp.py example with the two assignment orders, using NumPy 2.3.0. Compare the diagnostics and investigate the inferred protocol compatibility or subtyping-cache behavior involved in the order dependence. Done means both orders produce the same correct result, with regression coverage for the reported example.

Written by the indexing model from the issue text.

Assessment

Tech stack
numpy, python
Domain
devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.