Order dependent inference results with some protocols against numpy
- 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
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