From 2cfc68ec28b3773b138c763512f7c904a27e46fb Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Tue, 21 Apr 2026 23:13:11 +0000 Subject: [PATCH 1/4] GH-49826: [Python] Scalar arithmetic dunders return NotImplemented on unknown operand types pyarrow 24.0.0 added Scalar.__add__/__sub__/__mul__/__truediv__/__pow__ (and bitwise ops) that unconditionally call pc.call_function. For operand types compute doesn't recognize, call_function raises TypeError -- Python's data model only triggers reflected-operator fallback when the forward operator returns NotImplemented, not when it raises. Any user class that used to handle `pyarrow.Scalar + obj` via __radd__ was therefore broken (GH-49826). Route every binary dunder through a small _binop_or_notimplemented helper that catches the TypeError from _pack_compute_args and returns NotImplemented instead. Unary __neg__/__abs__ are unaffected. Closes GH-49826. Signed-off-by: SAY-5 --- python/pyarrow/scalar.pxi | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/python/pyarrow/scalar.pxi b/python/pyarrow/scalar.pxi index a6377b2bb707..7d58d1bdb71c 100644 --- a/python/pyarrow/scalar.pxi +++ b/python/pyarrow/scalar.pxi @@ -199,37 +199,48 @@ 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. + try: + return _pc().call_function(op_name, [left, right]) + except TypeError: + return NotImplemented _NULL = NA = None From b97b72a281a006a78890c8ac5df8a5dae6330746 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Sat, 2 May 2026 03:26:11 +0000 Subject: [PATCH 2/4] [Python] Extend NotImplemented fallback to Array.__add__ et al, add radd tests Per @AlenkaF review on GH-49833. --- python/pyarrow/array.pxi | 28 ++++++++++++++++++---------- python/pyarrow/tests/test_array.py | 10 ++++++++++ python/pyarrow/tests/test_scalars.py | 10 ++++++++++ 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/python/pyarrow/array.pxi b/python/pyarrow/array.pxi index b7f3a46f9e14..35b64f3a98c6 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,39 @@ 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. + try: + return _pc().call_function(op_name, [left, right]) + except TypeError: + return NotImplemented cdef _array_like_to_pandas(obj, options, types_mapper): 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" From 7354399f20f2db12f2a822465e9c84cc70357917 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Mon, 11 May 2026 20:05:45 +0000 Subject: [PATCH 3/4] chore: remove em-dashes from comments Signed-off-by: SAY-5 --- python/pyarrow/array.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/pyarrow/array.pxi b/python/pyarrow/array.pxi index 35b64f3a98c6..e5016a583f1d 100644 --- a/python/pyarrow/array.pxi +++ b/python/pyarrow/array.pxi @@ -2334,7 +2334,7 @@ cdef class Array(_PandasConvertible): def _array_binop_or_notimplemented(op_name, left, right): - # Same NotImplemented fallback as Scalar.__add__ et al — see GH-49826. + # Same NotImplemented fallback as Scalar.__add__ et al, see GH-49826. try: return _pc().call_function(op_name, [left, right]) except TypeError: From db38161c1b82932374ad9ca47444e45c73ce1f42 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Mon, 25 May 2026 07:06:35 +0000 Subject: [PATCH 4/4] fix(python): propagate TypeError for Arrow-to-Arrow binop mismatches Only return NotImplemented when the right operand is a non-Arrow type (enabling Python's reflected-operator fallback). When both operands are Arrow Scalar or Array instances, let TypeError from call_function surface so callers see a meaningful error instead of a silent NotImplemented. Signed-off-by: Sai Asish Y --- python/pyarrow/array.pxi | 6 +++++- python/pyarrow/scalar.pxi | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/python/pyarrow/array.pxi b/python/pyarrow/array.pxi index e5016a583f1d..7748a743bbbb 100644 --- a/python/pyarrow/array.pxi +++ b/python/pyarrow/array.pxi @@ -2335,9 +2335,13 @@ cdef class Array(_PandasConvertible): 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: + except TypeError as e: + if isinstance(right, (Scalar, Array)): + raise return NotImplemented diff --git a/python/pyarrow/scalar.pxi b/python/pyarrow/scalar.pxi index 7d58d1bdb71c..a22bbbaa3e0e 100644 --- a/python/pyarrow/scalar.pxi +++ b/python/pyarrow/scalar.pxi @@ -237,9 +237,16 @@ def _binop_or_notimplemented(op_name, left, right): # 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: + except TypeError as e: + if isinstance(right, (Scalar, Array)): + raise return NotImplemented