This is an automated email from the ASF dual-hosted git repository.

raulcd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new 65b7d24736c GH-49826: [Python] Return NotImplemented from Scalar/Array 
arithmetic dunders for unsupported types (#49845)
65b7d24736c is described below

commit 65b7d24736ce9000f2ab6c2fa45112ee50c26b61
Author: Alexandros Anastasiou <[email protected]>
AuthorDate: Wed Sep 30 10:57:57 2026 +0200

    GH-49826: [Python] Return NotImplemented from Scalar/Array arithmetic 
dunders for unsupported types (#49845)
    
    ### Rationale for this change
    
    In pyarrow 24.0.0, `Scalar` and `Array` gained arithmetic dunder methods 
(#32007) that unconditionally dispatch to `pyarrow.compute.call_function`. When 
the other operand is an unrecognized type, `_pack_compute_args` raises 
`TypeError` instead of returning `NotImplemented`. This prevents Python from 
falling back to the other operand's reflected methods (`__radd__`, `__rsub__`, 
etc.), breaking downstream libraries that relied on this protocol.
    
    ### What changes are included in this PR?
    
    - Adds a `_compute_binary_op` helper in `scalar.pxi` that wraps 
`call_function` in a `try/except TypeError` and returns `NotImplemented` on 
failure. This follows the same pattern already used by `Scalar.__eq__` and 
`Array.__eq__`.
    - Updates all binary arithmetic and bitwise dunders on both `Scalar` 
(`scalar.pxi`) and `Array` (`array.pxi`) to use this helper.
    - Adds parametrized tests in `test_scalars.py` and `test_array.py` 
verifying that reflected operators on custom types are correctly invoked.
    
    ### Are these changes tested?
    
    Yes. New parametrized tests 
(`test_dunders_return_notimplemented_for_unknown_types`) cover all 10 binary 
operators for both `Scalar` and `Array`. The bug was also manually reproduced 
against pyarrow 24.0.0 to confirm the tests exercise the right code path.
    
    ### Are there any user-facing changes?
    
    Yes. `Scalar` and `Array` arithmetic dunders now return `NotImplemented` 
instead of raising `TypeError` when the other operand is not a recognized 
Arrow/NumPy type. This restores the pre-24.0.0 behavior where Python would fall 
back to the other operand's reflected method.
    
    ### AI-generated code disclosure
    
    This PR was developed with assistance from an AI coding tool (Claude, 
Anthropic). All changes have been reviewed, understood, and verified.
    
    * GitHub Issue: #49826
    Closes #49826
    
    Lead-authored-by: AlenkaF <[email protected]>
    Co-authored-by: Alexandros Anastasiou <[email protected]>
    Signed-off-by: Raúl Cumplido <[email protected]>
---
 python/pyarrow/array.pxi             | 33 +++++++++++++++++++++++----------
 python/pyarrow/scalar.pxi            | 20 ++++++++++----------
 python/pyarrow/tests/test_array.py   | 36 ++++++++++++++++++++++++++++++++++++
 python/pyarrow/tests/test_scalars.py | 36 ++++++++++++++++++++++++++++++++++++
 4 files changed, 105 insertions(+), 20 deletions(-)

diff --git a/python/pyarrow/array.pxi b/python/pyarrow/array.pxi
index 865d2079fc0..223b77dd4fa 100644
--- a/python/pyarrow/array.pxi
+++ b/python/pyarrow/array.pxi
@@ -1124,6 +1124,19 @@ cdef PandasOptions _convert_pandas_options(dict options):
     return result
 
 
+def _compute_binary_op(func_name, left, right):
+    """
+    Helper for arithmetic/bitwise dunder methods.
+
+    Only use for ops that can't raise ArrowTypeError as it
+    subclasses TypeError, so will get swallowed.
+    """
+    try:
+        return _pc().call_function(func_name, [left, right])
+    except TypeError:
+        return NotImplemented
+
+
 cdef class Array(_PandasConvertible):
     """
     The base class for all Arrow arrays.
@@ -2445,15 +2458,15 @@ cdef class Array(_PandasConvertible):
 
     def __add__(self, object other):
         self._assert_cpu()
-        return _pc().call_function('add_checked', [self, other])
+        return _compute_binary_op('add_checked', self, other)
 
     def __truediv__(self, object other):
         self._assert_cpu()
-        return _pc().call_function('divide_checked', [self, other])
+        return _compute_binary_op('divide_checked', self, other)
 
     def __mul__(self, object other):
         self._assert_cpu()
-        return _pc().call_function('multiply_checked', [self, other])
+        return _compute_binary_op('multiply_checked', self, other)
 
     def __neg__(self):
         self._assert_cpu()
@@ -2461,31 +2474,31 @@ cdef class Array(_PandasConvertible):
 
     def __pow__(self, object other):
         self._assert_cpu()
-        return _pc().call_function('power_checked', [self, other])
+        return _compute_binary_op('power_checked', self, other)
 
     def __sub__(self, object other):
         self._assert_cpu()
-        return _pc().call_function('subtract_checked', [self, other])
+        return _compute_binary_op('subtract_checked', self, other)
 
     def __and__(self, object other):
         self._assert_cpu()
-        return _pc().call_function('bit_wise_and', [self, other])
+        return _compute_binary_op('bit_wise_and', self, other)
 
     def __or__(self, object other):
         self._assert_cpu()
-        return _pc().call_function('bit_wise_or', [self, other])
+        return _compute_binary_op('bit_wise_or', self, other)
 
     def __xor__(self, object other):
         self._assert_cpu()
-        return _pc().call_function('bit_wise_xor', [self, other])
+        return _compute_binary_op('bit_wise_xor', self, other)
 
     def __lshift__(self, object other):
         self._assert_cpu()
-        return _pc().call_function('shift_left_checked', [self, other])
+        return _compute_binary_op('shift_left_checked', self, other)
 
     def __rshift__(self, object other):
         self._assert_cpu()
-        return _pc().call_function('shift_right_checked', [self, other])
+        return _compute_binary_op('shift_right_checked', self, other)
 
 
 cdef _array_like_to_pandas(obj, options, types_mapper):
diff --git a/python/pyarrow/scalar.pxi b/python/pyarrow/scalar.pxi
index 863ab2b66a8..f96fb40572b 100644
--- a/python/pyarrow/scalar.pxi
+++ b/python/pyarrow/scalar.pxi
@@ -199,37 +199,37 @@ 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 _compute_binary_op('add_checked', self, other)
 
     def __truediv__(self, object other):
-        return _pc().call_function('divide_checked', [self, other])
+        return _compute_binary_op('divide_checked', self, other)
 
     def __mul__(self, object other):
-        return _pc().call_function('multiply_checked', [self, other])
+        return _compute_binary_op('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 _compute_binary_op('power_checked', self, other)
 
     def __sub__(self, object other):
-        return _pc().call_function('subtract_checked', [self, other])
+        return _compute_binary_op('subtract_checked', self, other)
 
     def __and__(self, object other):
-        return _pc().call_function('bit_wise_and', [self, other])
+        return _compute_binary_op('bit_wise_and', self, other)
 
     def __or__(self, object other):
-        return _pc().call_function('bit_wise_or', [self, other])
+        return _compute_binary_op('bit_wise_or', self, other)
 
     def __xor__(self, object other):
-        return _pc().call_function('bit_wise_xor', [self, other])
+        return _compute_binary_op('bit_wise_xor', self, other)
 
     def __lshift__(self, object other):
-        return _pc().call_function('shift_left_checked', [self, other])
+        return _compute_binary_op('shift_left_checked', self, other)
 
     def __rshift__(self, object other):
-        return _pc().call_function('shift_right_checked', [self, other])
+        return _compute_binary_op('shift_right_checked', self, other)
 
 
 _NULL = NA = None
diff --git a/python/pyarrow/tests/test_array.py 
b/python/pyarrow/tests/test_array.py
index eb81a4409e8..87040c0800f 100644
--- a/python/pyarrow/tests/test_array.py
+++ b/python/pyarrow/tests/test_array.py
@@ -21,6 +21,7 @@ import decimal
 import hypothesis as h
 import hypothesis.strategies as st
 import itertools
+import operator
 import pytest
 import struct
 import subprocess
@@ -4669,3 +4670,38 @@ def test_dictionary_uint64_index_to_pandas():
     result = arr.to_pandas()
     assert list(result.cat.categories) == ["a", "b"]
     assert result.cat.codes.tolist() == [0, 1, -1, 0]
+
+
[email protected]("op", [
+    operator.add,
+    operator.sub,
+    operator.mul,
+    operator.truediv,
+    operator.pow,
+    operator.and_,
+    operator.or_,
+    operator.xor,
+    operator.lshift,
+    operator.rshift,
+])
+def test_arithmetic_dunders_unknown_types(op):
+    # GH-49826
+    class MyObj:
+        def __radd__(self, other):
+            return "reflected"
+
+        __rsub__ = __rmul__ = __rtruediv__ = __rpow__ = __radd__
+        __rand__ = __ror__ = __rxor__ = __rlshift__ = __rrshift__ = __radd__
+
+    assert op(pa.array([1, 2, 3]), MyObj()) == "reflected"
+
+    # If NotImplemented is returned for both sides of the operation
+    # Python will fallback to a TypeError
+    with pytest.raises(TypeError, match="unsupported operand type\\(s\\)"):
+        op(pa.array([1, 2, 3]), object())
+
+
+def test_arithmetic_dunder_raises_arrow_invalid():
+    # GH-49826
+    with pytest.raises(pa.ArrowInvalid, match="divide by zero"):
+        pa.array([1, 2, 3]) / pa.scalar(0)
diff --git a/python/pyarrow/tests/test_scalars.py 
b/python/pyarrow/tests/test_scalars.py
index 08f9fcd55ce..533a0585052 100644
--- a/python/pyarrow/tests/test_scalars.py
+++ b/python/pyarrow/tests/test_scalars.py
@@ -17,6 +17,7 @@
 
 import datetime
 import decimal
+import operator
 import pytest
 import weakref
 from collections.abc import Sequence, Mapping
@@ -1051,3 +1052,38 @@ def test_dunders_checked_overflow():
         scl ** scl
     with pytest.raises(pa.ArrowInvalid, match=error_match):
         scl * scl
+
+
[email protected]("op", [
+    operator.add,
+    operator.sub,
+    operator.mul,
+    operator.truediv,
+    operator.pow,
+    operator.and_,
+    operator.or_,
+    operator.xor,
+    operator.lshift,
+    operator.rshift,
+])
+def test_arithmetic_dunders_unknown_types(op):
+    # GH-49826
+    class MyObj:
+        def __radd__(self, other):
+            return "reflected"
+
+        __rsub__ = __rmul__ = __rtruediv__ = __rpow__ = __radd__
+        __rand__ = __ror__ = __rxor__ = __rlshift__ = __rrshift__ = __radd__
+
+    assert op(pa.scalar(5), MyObj()) == "reflected"
+
+    # If NotImplemented is returned for both sides of the operation
+    # Python will fallback to a TypeError
+    with pytest.raises(TypeError, match="unsupported operand type\\(s\\)"):
+        op(pa.scalar(1), object())
+
+
+def test_arithmetic_dunder_raises_arrow_invalid():
+    # GH-49826
+    with pytest.raises(pa.ArrowInvalid, match="divide by zero"):
+        pa.scalar(1) / pa.scalar(0)

Reply via email to