Skip to content

Conversation

MengAiDev
Copy link

Fixes #29486

Description

This PR addresses an issue with static typing of reverse operators in NumPy's ndarray class. When using objects that implement both __array__ and __array_ufunc__ (like pandas Series) in reverse operations with NumPy arrays, the static typing incorrectly reports the result as an ndarray instead of the appropriate type from the left-hand side object.

The solution implements a Protocol-based approach that adds overloads to the reverse operators (__radd__, __rsub__, __rmul__, etc.) in the ndarray class. These overloads check if the left-hand side operand implements the corresponding forward operator and, if so, delegate to that operator, returning its result type.

…func__

Fixes numpy#29486

## Description
This PR addresses an issue with static typing of reverse operators in NumPy's `ndarray` class. When using objects that implement both `__array__` and `__array_ufunc__` (like pandas Series) in reverse operations with NumPy arrays, the static typing incorrectly reports the result as an `ndarray` instead of the appropriate type from the left-hand side object.

The solution implements a Protocol-based approach that adds overloads to the reverse operators (`__radd__`, `__rsub__`, `__rmul__`, etc.) in the `ndarray` class. These overloads check if the left-hand side operand implements the corresponding forward operator and, if so, delegate to that operator, returning its result type.

## Changes
1. Added a `_CanAdd` Protocol in `numpy/_typing/_callable.pyi`
2. Added new overloads to reverse operators in `numpy/__init__.pyi` that use the Protocol
3. Added necessary imports for the new Protocol and TypeVar

## Testing
The changes have been syntax-checked with `python -m compileall` and maintain backward compatibility with existing code.

For a detailed explanation of the problem and solution, see ISSUE_29486_SOLUTION.md
@mattip mattip changed the title Fix reverse operators typing for objects with __array__ and __array_ufunc__ TYP: Fix reverse operators typing for objects with __array__ and __array_ufunc__ Aug 19, 2025

This comment has been minimized.

@jorenham jorenham self-requested a review August 19, 2025 12:48
@@ -207,6 +208,7 @@ from typing import (
SupportsIndex,
TypeAlias,
TypedDict,
TypeVar,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TypeVar should be imported from typing_extensions because we use the PEP 696 default kwarg, which requires Python 3.13+

@@ -42,6 +42,7 @@ from ._scalars import _BoolLike_co, _IntLike_co, _NumberLike_co

_T1 = TypeVar("_T1")
_T2 = TypeVar("_T2")
_T = TypeVar("_T")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see how you might want to use _CanAdd for __radd__, but it doesn't make much sense to also use that for other reflected binops like __rpow__

Co-authored-by: Joren Hammudoglu <jhammudoglu@gmail.com>

This comment has been minimized.

@jorenham
Copy link
Member

Thanks for this PR; it's always good to see interest in improving NumPy's type annotations.

But keep in mind that every commit you push notifies us, and that running the CI isn't free. So please run the mypy tests locally first (spin mypy) before you push any changes.

Also note that this is a very difficult problem to solve, and I'm still not sure what the right approach is, even though I do this for a living. Don't take this the wrong way, but based on your changes so far, it looks to me like you're not an expert in writing typing stubs. I'm a bit worried that the time that your generously investing in this might go to waste, due to its complexity.

So perhaps it would be more efficient (and more fun) to work on a different numpy typing issue? Some options that come to mind are #29595, or annotating one of the untyped numpy.ma functions.

Copy link

Diff from mypy_primer, showing the effect of this PR on type check results on a corpus of open source code:

static-frame (https://github.com/static-frame/static-frame)
+ static_frame/core/rank.py:106: error: Unused "type: ignore" comment  [unused-ignore]

xarray (https://github.com/pydata/xarray)
+ xarray/computation/weighted.py: note: In function "_weighted_quantile":
+ xarray/computation/weighted.py:333: error: Incompatible types in assignment (expression has type "int", variable has type "ndarray[tuple[Any, ...], dtype[Any]]")  [assignment]
+ xarray/computation/weighted.py:337: error: Incompatible types in assignment (expression has type "float", variable has type "ndarray[tuple[Any, ...], dtype[Any]]")  [assignment]
+ xarray/computation/weighted.py:341: error: Incompatible types in assignment (expression has type "float", variable has type "ndarray[tuple[Any, ...], dtype[Any]]")  [assignment]
+ xarray/computation/weighted.py:343: error: Incompatible types in assignment (expression has type "float", variable has type "ndarray[tuple[Any, ...], dtype[Any]]")  [assignment]
+ xarray/tests/test_namedarray.py: note: In member "test_real_and_imag" of class "TestNamedArray":
+ xarray/tests/test_namedarray.py:274: error: Incompatible types in assignment (expression has type "ndarray[tuple[int], dtype[float64]]", variable has type "ndarray[Any, dtype[complex128]]")  [assignment]
+ xarray/tests/test_namedarray.py: note: In class "TestNamedArray":
+ xarray/tests/test_variable.py: note: In member "test_copy" of class "VariableSubclassobjects":
+ xarray/tests/test_variable.py:555: error: "Never" has no attribute "astype"  [attr-defined]
+ xarray/tests/test_variable.py: note: At top level:
+ xarray/tests/test_duck_array_ops.py:892: error: Need type annotation for "expected"  [var-annotated]
+ xarray/tests/test_duck_array_ops.py:908: error: Need type annotation for "expected2"  [var-annotated]
+ xarray/tests/test_duck_array_ops.py: note: In function "test_datetime_to_numeric_potential_overflow":
+ xarray/tests/test_duck_array_ops.py:980: error: Need type annotation for "expected"  [var-annotated]
+ xarray/tests/test_dataset.py: note: In member "test_constructor" of class "TestDataset":
+ xarray/tests/test_dataset.py:475: error: Need type annotation for "x1"  [var-annotated]
+ xarray/tests/test_cftimeindex.py: note: In function "test_asi8":
+ xarray/tests/test_cftimeindex.py:1380: error: Need type annotation for "expected"  [var-annotated]
+ xarray/tests/test_backends.py: note: In function "test_use_cftime_standard_calendar_default_in_range":
+ xarray/tests/test_backends.py:6639: error: Need type annotation for "decoded_x"  [var-annotated]
+ xarray/tests/test_backends.py:6640: error: Need type annotation for "decoded_time"  [var-annotated]
+ xarray/tests/test_backends.py: note: In function "test_use_cftime_false_standard_calendar_in_range":
+ xarray/tests/test_backends.py:6732: error: Need type annotation for "decoded_x"  [var-annotated]
+ xarray/tests/test_backends.py:6733: error: Need type annotation for "decoded_time"  [var-annotated]

arviz (https://github.com/arviz-devs/arviz)
+ arviz/stats/ecdf_utils.py:69: error: "Never" has no attribute "astype"  [attr-defined]
+ examples/matplotlib/mpl_plot_hdi.py:15: error: Need type annotation for "y_data"  [var-annotated]
+ examples/bokeh/bokeh_plot_hdi.py:12: error: Need type annotation for "y_data"  [var-annotated]

optuna (https://github.com/optuna/optuna)
+ optuna/samplers/_tpe/_truncnorm.py:75: error: Incompatible return value type (got "float", expected "ndarray[tuple[Any, ...], dtype[Any]]")  [return-value]
+ tests/hypervolume_tests/test_wfg.py:22: error: Need type annotation for "r"  [var-annotated]
+ tests/hypervolume_tests/test_wfg.py:35: error: Need type annotation for "r"  [var-annotated]
+ tests/hypervolume_tests/test_wfg.py:53: error: Need type annotation for "r"  [var-annotated]
+ tests/hypervolume_tests/test_wfg.py:63: error: Need type annotation for "r"  [var-annotated]
+ tests/hypervolume_tests/test_wfg.py:73: error: Need type annotation for "r"  [var-annotated]
+ tests/hypervolume_tests/test_wfg.py:81: error: Need type annotation for "r"  [var-annotated]
+ tests/hypervolume_tests/test_wfg.py:91: error: Need type annotation for "r"  [var-annotated]
+ tests/hypervolume_tests/test_wfg.py:101: error: Need type annotation for "s"  [var-annotated]
+ optuna/importance/_ped_anova/evaluator.py:43: error: Need type annotation for "loss_values"  [var-annotated]

scipy (https://github.com/scipy/scipy)
+ scipy/io/matlab/tests/test_mio.py:46: error: Need type annotation for "theta"  [var-annotated]
+ scipy/interpolate/_cubic.py:562: error: Unsupported target for indexed assignment ("Never")  [index]
+ scipy/_lib/cobyqa/examples/powell2015.py:49: error: Need type annotation for "bub"  [var-annotated]

pandas-stubs (https://github.com/pandas-dev/pandas-stubs)
+ tests/test_plotting.py:201: error: Unsupported left operand type for + ("Never")  [operator]
+ tests/series/arithmetic/bool/test_sub.py:87: error: Expression is of type "Any", not "Never"  [assert-type]

hydpy (https://github.com/hydpy-dev/hydpy)
+ hydpy/auxs/statstools.py:1053: error: Incompatible return value type (got "float", expected "ndarray[tuple[Any, ...], dtype[float64]]")  [return-value]

pandas (https://github.com/pandas-dev/pandas)
+ pandas/core/dtypes/missing.py:590: error: Need type annotation for "taker"  [var-annotated]
+ pandas/io/formats/format.py:1603: error: "Never" has no attribute "round"  [attr-defined]
+ pandas/io/formats/format.py:1611: error: Need type annotation for "unique_pcts"  [var-annotated]
+ pandas/io/formats/format.py:1614: error: Value of type "Never" is not indexable  [index]
+ pandas/io/formats/format.py:1616: error: Value of type "Never" is not indexable  [index]
+ pandas/core/indexing.py:1889: error: Need type annotation for "taker"  [var-annotated]
+ pandas/core/internals/managers.py:2484: error: Need type annotation for "empty_arr"  [var-annotated]
+ pandas/core/indexes/base.py:6171: error: Need type annotation for "no_matches"  [var-annotated]
+ pandas/core/groupby/generic.py:1033: error: Need type annotation for "idchanges"  [var-annotated]
+ pandas/core/reshape/reshape.py:996: error: Need type annotation for "idxs"  [var-annotated]

colour (https://github.com/colour-science/colour)
+ colour/algebra/interpolation.py:308: error: Unsupported left operand type for + ("Never")  [operator]
+ colour/algebra/interpolation.py:309: error: Unsupported left operand type for + ("Never")  [operator]
+ colour/temperature/kang2002.py:177: error: Unsupported left operand type for - ("Never")  [operator]
+ colour/temperature/kang2002.py:178: error: Unsupported left operand type for - ("Never")  [operator]
+ colour/temperature/kang2002.py:179: error: Unsupported left operand type for - ("Never")  [operator]
+ colour/models/yrg.py:119: error: Unsupported left operand type for - ("Never")  [operator]
+ colour/models/yrg.py:120: error: Unsupported left operand type for + ("Never")  [operator]
+ colour/models/rgb/transfer_functions/log.py:321: error: Redundant cast to "float"  [redundant-cast]
+ colour/models/rgb/transfer_functions/dcdm.py:174: error: Need type annotation for "XYZ"  [var-annotated]
+ colour/difference/delta_e.py:404: error: Need type annotation for "delta_theta"  [var-annotated]
+ colour/contrast/barten1999.py:246: error: Need type annotation for "E"  [var-annotated]
+ colour/colorimetry/yellowness.py:280: error: Need type annotation for "WI"  [var-annotated]
+ colour/appearance/llab.py:555: error: Need type annotation for "z"  [var-annotated]
+ colour/appearance/llab.py:559: error: Need type annotation for "a"  [var-annotated]
+ colour/appearance/llab.py:560: error: Need type annotation for "b"  [var-annotated]
+ colour/appearance/hunt.py:572: error: Incompatible return value type (got "floating[_16Bit] | floating[_32Bit] | float64", expected "ndarray[tuple[Any, ...], dtype[floating[_16Bit] | floating[_32Bit] | float64]]")  [return-value]
+ colour/appearance/hunt.py:603: error: Incompatible return value type (got "floating[_16Bit] | floating[_32Bit] | float64", expected "ndarray[tuple[Any, ...], dtype[floating[_16Bit] | floating[_32Bit] | float64]]")  [return-value]
+ colour/appearance/hunt.py:766: error: Incompatible return value type (got "float", expected "ndarray[tuple[Any, ...], dtype[floating[_16Bit] | floating[_32Bit] | float64]]")  [return-value]
+ colour/appearance/hunt.py:1187: error: Incompatible types in assignment (expression has type "floating[_16Bit] | float64 | floating[_32Bit]", variable has type "float")  [assignment]
+ colour/appearance/hunt.py:1245: error: Incompatible return value type (got "floating[_16Bit] | float64 | floating[_32Bit]", expected "ndarray[tuple[Any, ...], dtype[floating[_16Bit] | floating[_32Bit] | float64]]")  [return-value]
+ colour/appearance/hke.py:226: error: Need type annotation for "theta_2"  [var-annotated]
+ colour/appearance/hke.py:226: error: Need type annotation for "theta_3"  [var-annotated]
+ colour/appearance/hke.py:226: error: Need type annotation for "theta_4"  [var-annotated]
+ colour/models/rgb/transfer_functions/sony.py:330: error: Incompatible return value type (got "float", expected "ndarray[tuple[Any, ...], dtype[floating[_16Bit] | floating[_32Bit] | float64]]")  [return-value]
+ colour/appearance/rlab.py:280: error: Need type annotation for "LR"  [var-annotated]
+ colour/colorimetry/luminance.py:206: error: Unsupported left operand type for - ("Never")  [operator]
+ colour/models/hdr_cie_lab.py:228: error: Need type annotation for "a_hdr"  [var-annotated]
+ colour/models/hdr_cie_lab.py:229: error: Need type annotation for "b_hdr"  [var-annotated]
+ colour/models/cie_luv.py:139: error: Unsupported left operand type for - ("Never")  [operator]
+ colour/models/cie_luv.py:140: error: Unsupported left operand type for - ("Never")  [operator]
+ colour/models/cie_luv.py:216: error: Need type annotation for "b"  [var-annotated]
+ colour/models/cie_lab.py:116: error: Need type annotation for "a"  [var-annotated]
+ colour/models/cie_lab.py:117: error: Need type annotation for "b"  [var-annotated]
+ colour/colorimetry/illuminants.py:360: error: Unsupported left operand type for + ("Never")  [operator]
+ colour/temperature/mccamy1992.py:81: error: Unsupported left operand type for + ("Never")  [operator]
+ colour/phenomena/rayleigh.py:222: error: Incompatible return value type (got "floating[_16Bit] | floating[_32Bit] | float64", expected "ndarray[tuple[Any, ...], dtype[floating[_16Bit] | floating[_32Bit] | float64]]")  [return-value]
+ colour/phenomena/rayleigh.py:368: error: Unsupported left operand type for + ("Never")  [operator]
+ colour/phenomena/rayleigh.py:406: error: Unsupported left operand type for + ("Never")  [operator]
+ colour/phenomena/rayleigh.py:480: error: Incompatible return value type (got "float", expected "ndarray[tuple[Any, ...], dtype[floating[_16Bit] | floating[_32Bit] | float64]]")  [return-value]
+ colour/appearance/scam.py:270: error: Need type annotation for "z"  [var-annotated]
+ colour/appearance/scam.py:426: error: Need type annotation for "z"  [var-annotated]
+ colour/appearance/scam.py:440: error: Need type annotation for "I"  [var-annotated]
+ colour/appearance/ciecam02.py:625: error: Need type annotation for "N_bb"  [var-annotated]
+ colour/appearance/ciecam02.py:625: error: Need type annotation for "N_cb"  [var-annotated]
- colour/notation/munsell.py:1955: error: Incompatible types in assignment (expression has type "int", variable has type "ndarray[tuple[Any, ...], dtype[floating[_16Bit] | floating[_32Bit] | float64]]")  [assignment]
+ colour/notation/munsell.py:440: error: Need type annotation for "V"  [var-annotated]
+ colour/appearance/hellwig2022.py:384: error: Need type annotation for "Q_HK"  [var-annotated]
+ colour/appearance/hellwig2022.py:762: error: Need type annotation for "_2_h"  [var-annotated]
+ colour/appearance/hellwig2022.py:763: error: Need type annotation for "_3_h"  [var-annotated]
+ colour/appearance/hellwig2022.py:764: error: Need type annotation for "_4_h"  [var-annotated]
+ colour/appearance/hellwig2022.py:961: error: Need type annotation for "P_p_1"  [var-annotated]
+ colour/appearance/ciecam16.py:689: error: Unsupported left operand type for / ("Never")  [operator]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Pending authors' response
Development

Successfully merging this pull request may close these issues.

TYP: Reverse operators with pandas returning wrong types
3 participants