From 98bf1dd876914ea87445f4e411b037416c4d172e Mon Sep 17 00:00:00 2001 From: Alexandros Anastasiou Date: Wed, 22 Apr 2026 22:17:02 +0100 Subject: [PATCH 1/4] GH-49826: [Python] Return NotImplemented from Scalar arithmetic dunders for unsupported types --- python/pyarrow/scalar.pxi | 27 +++++++++++++++++---------- python/pyarrow/tests/test_scalars.py | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/python/pyarrow/scalar.pxi b/python/pyarrow/scalar.pxi index 863ab2b66a8c..349b990642dc 100644 --- a/python/pyarrow/scalar.pxi +++ b/python/pyarrow/scalar.pxi @@ -21,6 +21,13 @@ from uuid import UUID from collections.abc import Sequence, Mapping +def _compute_binary_op(func_name, left, right): + try: + return _pc().call_function(func_name, [left, right]) + except TypeError: + return NotImplemented + + cdef class Scalar(_Weakrefable): """ The base class for scalars. @@ -199,37 +206,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_scalars.py b/python/pyarrow/tests/test_scalars.py index 08f9fcd55ce0..4ca541f4d7ce 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,27 @@ def test_dunders_checked_overflow(): scl ** scl with pytest.raises(pa.ArrowInvalid, match=error_match): scl * scl + + +@pytest.mark.parametrize("op", [ + operator.add, + operator.sub, + operator.mul, + operator.truediv, + operator.pow, + operator.and_, + operator.or_, + operator.xor, + operator.lshift, + operator.rshift, +]) +def test_dunders_return_notimplemented_for_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" From dac523f0ac5175851996b8493823c4d4f631a81a Mon Sep 17 00:00:00 2001 From: Alexandros Anastasiou Date: Wed, 22 Apr 2026 22:17:05 +0100 Subject: [PATCH 2/4] GH-49826: [Python] Return NotImplemented from Array arithmetic dunders for unsupported types --- python/pyarrow/array.pxi | 20 ++++++++++---------- python/pyarrow/tests/test_array.py | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/python/pyarrow/array.pxi b/python/pyarrow/array.pxi index 3060c533255b..fc99178d6261 100644 --- a/python/pyarrow/array.pxi +++ b/python/pyarrow/array.pxi @@ -2440,15 +2440,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() @@ -2456,31 +2456,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/tests/test_array.py b/python/pyarrow/tests/test_array.py index a1e3616c9cea..65319ab199ba 100644 --- a/python/pyarrow/tests/test_array.py +++ b/python/pyarrow/tests/test_array.py @@ -21,6 +21,7 @@ import hypothesis as h import hypothesis.strategies as st import itertools +import operator import pytest import struct import subprocess @@ -4612,3 +4613,25 @@ 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] +@pytest.mark.parametrize("op", [ + operator.add, + operator.sub, + operator.mul, + operator.truediv, + operator.pow, + operator.and_, + operator.or_, + operator.xor, + operator.lshift, + operator.rshift, +]) +def test_dunders_return_notimplemented_for_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" From 50388a944d3e39b63adc9684342324cba965bfe2 Mon Sep 17 00:00:00 2001 From: Alexandros Anastasiou Date: Thu, 23 Apr 2026 17:31:42 +0100 Subject: [PATCH 3/4] GH-49826: [Python] Pre-validate operand type instead of broad TypeError catch --- python/pyarrow/scalar.pxi | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/python/pyarrow/scalar.pxi b/python/pyarrow/scalar.pxi index 349b990642dc..c637fe44e497 100644 --- a/python/pyarrow/scalar.pxi +++ b/python/pyarrow/scalar.pxi @@ -21,11 +21,23 @@ from uuid import UUID from collections.abc import Sequence, Mapping -def _compute_binary_op(func_name, left, right): +def _is_valid_compute_operand(value): + if isinstance(value, (Scalar, Array, ChunkedArray, RecordBatch, Table, + list, tuple)): + return True + if np is not None and isinstance(value, np.ndarray): + return True try: - return _pc().call_function(func_name, [left, right]) - except TypeError: + scalar(value) + except Exception: + return False + return True + + +def _compute_binary_op(func_name, left, right): + if not _is_valid_compute_operand(right): return NotImplemented + return _pc().call_function(func_name, [left, right]) cdef class Scalar(_Weakrefable): From ce9361719e587da66f257280304436f86ec2a79b Mon Sep 17 00:00:00 2001 From: AlenkaF Date: Fri, 11 Sep 2026 11:54:50 +0200 Subject: [PATCH 4/4] Do a bit of cleaning --- python/pyarrow/array.pxi | 13 +++++++++++++ python/pyarrow/scalar.pxi | 19 ------------------- python/pyarrow/tests/test_array.py | 13 ++++++++++++- python/pyarrow/tests/test_scalars.py | 11 ++++++++++- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/python/pyarrow/array.pxi b/python/pyarrow/array.pxi index fc99178d6261..ce248f4793e6 100644 --- a/python/pyarrow/array.pxi +++ b/python/pyarrow/array.pxi @@ -1123,6 +1123,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. diff --git a/python/pyarrow/scalar.pxi b/python/pyarrow/scalar.pxi index c637fe44e497..f96fb40572b9 100644 --- a/python/pyarrow/scalar.pxi +++ b/python/pyarrow/scalar.pxi @@ -21,25 +21,6 @@ from uuid import UUID from collections.abc import Sequence, Mapping -def _is_valid_compute_operand(value): - if isinstance(value, (Scalar, Array, ChunkedArray, RecordBatch, Table, - list, tuple)): - return True - if np is not None and isinstance(value, np.ndarray): - return True - try: - scalar(value) - except Exception: - return False - return True - - -def _compute_binary_op(func_name, left, right): - if not _is_valid_compute_operand(right): - return NotImplemented - return _pc().call_function(func_name, [left, right]) - - cdef class Scalar(_Weakrefable): """ The base class for scalars. diff --git a/python/pyarrow/tests/test_array.py b/python/pyarrow/tests/test_array.py index 65319ab199ba..9d3c6014a819 100644 --- a/python/pyarrow/tests/test_array.py +++ b/python/pyarrow/tests/test_array.py @@ -4613,6 +4613,8 @@ 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] + + @pytest.mark.parametrize("op", [ operator.add, operator.sub, @@ -4625,7 +4627,7 @@ def test_dictionary_uint64_index_to_pandas(): operator.lshift, operator.rshift, ]) -def test_dunders_return_notimplemented_for_unknown_types(op): +def test_arithmetic_dunders_unknown_types(op): # GH-49826 class MyObj: def __radd__(self, other): @@ -4635,3 +4637,12 @@ def __radd__(self, other): __rand__ = __ror__ = __rxor__ = __rlshift__ = __rrshift__ = __radd__ assert op(pa.array([1, 2, 3]), MyObj()) == "reflected" + + with pytest.raises(TypeError, match="unsupported operand type"): + 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 4ca541f4d7ce..a71915b73aa7 100644 --- a/python/pyarrow/tests/test_scalars.py +++ b/python/pyarrow/tests/test_scalars.py @@ -1066,7 +1066,7 @@ def test_dunders_checked_overflow(): operator.lshift, operator.rshift, ]) -def test_dunders_return_notimplemented_for_unknown_types(op): +def test_arithmetic_dunders_unknown_types(op): # GH-49826 class MyObj: def __radd__(self, other): @@ -1076,3 +1076,12 @@ def __radd__(self, other): __rand__ = __ror__ = __rxor__ = __rlshift__ = __rrshift__ = __radd__ assert op(pa.scalar(5), MyObj()) == "reflected" + + with pytest.raises(TypeError, match="unsupported operand type"): + 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)