diff --git a/python/pyarrow/array.pxi b/python/pyarrow/array.pxi index b7f3a46f9e14..7748a743bbbb 100644 --- a/python/pyarrow/array.pxi +++ b/python/pyarrow/array.pxi @@ -2290,15 +2290,15 @@ cdef class Array(_PandasConvertible): def __add__(self, object other): self._assert_cpu() - return _pc().call_function('add_checked', [self, other]) + return _array_binop_or_notimplemented('add_checked', self, other) def __truediv__(self, object other): self._assert_cpu() - return _pc().call_function('divide_checked', [self, other]) + return _array_binop_or_notimplemented('divide_checked', self, other) def __mul__(self, object other): self._assert_cpu() - return _pc().call_function('multiply_checked', [self, other]) + return _array_binop_or_notimplemented('multiply_checked', self, other) def __neg__(self): self._assert_cpu() @@ -2306,31 +2306,43 @@ cdef class Array(_PandasConvertible): def __pow__(self, object other): self._assert_cpu() - return _pc().call_function('power_checked', [self, other]) + return _array_binop_or_notimplemented('power_checked', self, other) def __sub__(self, object other): self._assert_cpu() - return _pc().call_function('subtract_checked', [self, other]) + return _array_binop_or_notimplemented('subtract_checked', self, other) def __and__(self, object other): self._assert_cpu() - return _pc().call_function('bit_wise_and', [self, other]) + return _array_binop_or_notimplemented('bit_wise_and', self, other) def __or__(self, object other): self._assert_cpu() - return _pc().call_function('bit_wise_or', [self, other]) + return _array_binop_or_notimplemented('bit_wise_or', self, other) def __xor__(self, object other): self._assert_cpu() - return _pc().call_function('bit_wise_xor', [self, other]) + return _array_binop_or_notimplemented('bit_wise_xor', self, other) def __lshift__(self, object other): self._assert_cpu() - return _pc().call_function('shift_left_checked', [self, other]) + return _array_binop_or_notimplemented('shift_left_checked', self, other) def __rshift__(self, object other): self._assert_cpu() - return _pc().call_function('shift_right_checked', [self, other]) + return _array_binop_or_notimplemented('shift_right_checked', self, other) + + +def _array_binop_or_notimplemented(op_name, left, right): + # Same NotImplemented fallback as Scalar.__add__ et al, see GH-49826. + # Only swallow TypeError for genuinely foreign types; propagate when + # the right operand is already Arrow-native so type errors are visible. + try: + return _pc().call_function(op_name, [left, right]) + except TypeError as e: + if isinstance(right, (Scalar, Array)): + raise + return NotImplemented cdef _array_like_to_pandas(obj, options, types_mapper): diff --git a/python/pyarrow/scalar.pxi b/python/pyarrow/scalar.pxi index a6377b2bb707..a22bbbaa3e0e 100644 --- a/python/pyarrow/scalar.pxi +++ b/python/pyarrow/scalar.pxi @@ -199,37 +199,55 @@ cdef class Scalar(_Weakrefable): return _pc().call_function('abs_checked', [self]) def __add__(self, object other): - return _pc().call_function('add_checked', [self, other]) + return _binop_or_notimplemented('add_checked', self, other) def __truediv__(self, object other): - return _pc().call_function('divide_checked', [self, other]) + return _binop_or_notimplemented('divide_checked', self, other) def __mul__(self, object other): - return _pc().call_function('multiply_checked', [self, other]) + return _binop_or_notimplemented('multiply_checked', self, other) def __neg__(self): return _pc().call_function('negate_checked', [self]) def __pow__(self, object other): - return _pc().call_function('power_checked', [self, other]) + return _binop_or_notimplemented('power_checked', self, other) def __sub__(self, object other): - return _pc().call_function('subtract_checked', [self, other]) + return _binop_or_notimplemented('subtract_checked', self, other) def __and__(self, object other): - return _pc().call_function('bit_wise_and', [self, other]) + return _binop_or_notimplemented('bit_wise_and', self, other) def __or__(self, object other): - return _pc().call_function('bit_wise_or', [self, other]) + return _binop_or_notimplemented('bit_wise_or', self, other) def __xor__(self, object other): - return _pc().call_function('bit_wise_xor', [self, other]) + return _binop_or_notimplemented('bit_wise_xor', self, other) def __lshift__(self, object other): - return _pc().call_function('shift_left_checked', [self, other]) + return _binop_or_notimplemented('shift_left_checked', self, other) def __rshift__(self, object other): - return _pc().call_function('shift_right_checked', [self, other]) + return _binop_or_notimplemented('shift_right_checked', self, other) + + +def _binop_or_notimplemented(op_name, left, right): + # Scalar arithmetic dunders must return NotImplemented for argument types + # pyarrow.compute does not recognize so Python's reflected-operator + # fallback (__radd__ etc.) kicks in on user-defined classes. + # See GH-49826. + # + # Only swallow TypeError for genuinely foreign types (non-Arrow). If + # the right operand is already an Arrow Scalar or Array, any TypeError + # from call_function (e.g. mismatched Arrow types) should propagate so + # the caller sees a meaningful error rather than a silent NotImplemented. + try: + return _pc().call_function(op_name, [left, right]) + except TypeError as e: + if isinstance(right, (Scalar, Array)): + raise + return NotImplemented _NULL = NA = None diff --git a/python/pyarrow/tests/test_array.py b/python/pyarrow/tests/test_array.py index a103519dc5ac..b27bce5945d9 100644 --- a/python/pyarrow/tests/test_array.py +++ b/python/pyarrow/tests/test_array.py @@ -4401,6 +4401,16 @@ def test_non_cpu_array(): arr.validate(full=True) +def test_array_radd_unknown_operand(): + # GH-49826: Array.__add__ on an unknown right operand must return + # NotImplemented so Python falls back to right.__radd__. + class WithRadd: + def __radd__(self, other): + return "radd-called" + + assert pa.array([1, 2]) + WithRadd() == "radd-called" + + def test_arithmetic_dunders(): # GH-32007 arr1 = pa.array([-1.1, 2.2, -3.3]) diff --git a/python/pyarrow/tests/test_scalars.py b/python/pyarrow/tests/test_scalars.py index 08f9fcd55ce0..0456e8397658 100644 --- a/python/pyarrow/tests/test_scalars.py +++ b/python/pyarrow/tests/test_scalars.py @@ -1022,6 +1022,16 @@ def test_bitwise_dunders(): assert (scl2 >> scl1).equals(pc.shift_right_checked(scl2, scl1)) +def test_scalar_radd_unknown_operand(): + # GH-49826: Scalar.__add__ on an unknown right operand must return + # NotImplemented so Python falls back to right.__radd__. + class WithRadd: + def __radd__(self, other): + return "radd-called" + + assert pa.scalar(1) + WithRadd() == "radd-called" + + def test_dunders_unmatching_types(): # GH-32007 error_match = r"Function '\w+' has no kernel matching input types"