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

pitrou 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 0083d11f61 GH-50326: [Python] Convert arrays to Python objects without 
per-element Scalars in to_pylist (#50327)
0083d11f61 is described below

commit 0083d11f61446ee57c260ce1af5e2a39ac745d48
Author: Liang-Chi Hsieh <[email protected]>
AuthorDate: Thu Jul 23 00:15:05 2026 +0800

    GH-50326: [Python] Convert arrays to Python objects without per-element 
Scalars in to_pylist (#50327)
    
    ### Rationale for this change
    
    `pa.Array.to_pylist()` converts one element at a time through 
`Array::GetScalar` plus a Python `Scalar` wrapper; for list types each row 
additionally allocates a Python `Array` wrapper for the row's values slice and 
a fresh generator before recursing per element. A `sample` profile shows ~20% 
of runtime in CPython GC (triggered by the per-row GC-tracked allocations), 
~25% in `GetScalar`, and only ~7% doing the useful work of creating the output 
objects — making `to_pylist` several tim [...]
    
    ### What changes are included in this PR?
    
    Following review feedback, this adds a general scalar-free conversion 
mechanism instead of per-type `to_pylist` overrides:
    
    - `Array` gains `cdef object _getitem_py(self, int64_t i)`, returning 
`self[i]` as a Python object. The base implementation is `GetScalar` + 
`Scalar.as_py`, so any type without a specialization behaves exactly as today 
(dates, times, timestamps, durations, decimals, dictionary, extension, unions, 
views, ...).
    - The baseline `Array.to_pylist` becomes a single loop over `_getitem_py`. 
`maps_as_pydicts != None` keeps the Scalar-based path, since map→dict 
conversion has per-entry duplicate-key semantics.
    - Specializations avoid all per-element Scalar and per-row Array-wrapper 
allocation:
      - integers and floats (a `type_id` switch on `NumericArray`; 
date/time/timestamp subclasses fall through to the exact base),
      - boolean,
      - string/binary and large variants (`GetValue` + `PyUnicode_DecodeUTF8` / 
`PyBytes_FromStringAndSize`, matching `str(buf, 'utf8')` / `to_pybytes()` 
exactly),
      - list/large_list/fixed_size_list (each row's list is built from the 
child's `_getitem_py` over the offset range; the wrapped child is cached on the 
parent array),
      - map (association list of key/value tuples, matching `MapScalar.as_py`),
      - struct (one dict per row; duplicate field names fall back to the Scalar 
path so they raise `ValueError` like `StructScalar.as_py`).
    
    Nested types compose without any per-row wrappers. 
`ChunkedArray.to_pylist`, `Table.to_pylist` and `ListScalar.as_py` delegate 
here and speed up automatically. Follow-up candidates: string/binary views, 
run-end-encoded, dictionary, a fast path for date32.
    
    Benchmarks (macOS arm64, M4 Max):
    
    | benchmark | before | after | speedup |
    |---|---|---|---|
    | flat `int64` with nulls (4M) | 0.39 s | 0.028 s | 14x (~7 ns/element, on 
par with `ndarray.tolist`) |
    | flat `string` (4M) | 0.83 s | 0.06 s | 14x |
    | `list<string>` (2M rows) | 1.93 s | 0.46 s | 4.2x |
    | `list<list<int32>>` (1M rows) | 2.10 s | 0.40 s | 5.2x |
    | `struct<int64,string>` (1M rows) | 0.91 s | 0.07 s | 13x |
    | `map<string,int64>` (1M rows) | 2.77 s | 0.74 s | 3.8x |
    
    ### Are these changes tested?
    
    `test_to_pylist_bulk_paths` (added here) compares against the per-scalar 
conversion with exact element types for representative arrays including sliced 
views. Additionally verified with a randomized differential test against 
`[x.as_py() for x in arr]` with exact-type equality: all integer widths (incl. 
values beyond 2^62), floats (NaN/inf), boolean, string/binary (+large, 
multibyte), all list kinds, nested lists, struct (incl. empty struct, 
duplicate-field-name `ValueError`), map (inc [...]
    
    ### Are there any user-facing changes?
    
    No behavior changes, only performance.
    
    * GitHub Issue: #50326
    
    This pull request and its description were written by Isaac.
    
    Lead-authored-by: Liang-Chi Hsieh <[email protected]>
    Co-authored-by: Isaac
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 python/pyarrow/array.pxi             | 157 ++++++++++++++++++++++++++++++++++-
 python/pyarrow/includes/libarrow.pxd |  90 +++++++++++---------
 python/pyarrow/lib.pxd               |   7 ++
 python/pyarrow/tests/test_array.py   |  58 +++++++++++++
 4 files changed, 270 insertions(+), 42 deletions(-)

diff --git a/python/pyarrow/array.pxi b/python/pyarrow/array.pxi
index e259a5e4fb..97cd9a8ebf 100644
--- a/python/pyarrow/array.pxi
+++ b/python/pyarrow/array.pxi
@@ -1864,7 +1864,24 @@ cdef class Array(_PandasConvertible):
         lst : list
         """
         self._assert_cpu()
-        return [x.as_py(maps_as_pydicts=maps_as_pydicts) for x in self]
+        cdef int64_t i, n = self.length()
+        if maps_as_pydicts is not None:
+            # Converting maps to dicts has per-entry semantics (duplicate-key
+            # detection); use the Scalar-based conversion for exact behavior.
+            # TODO(GH-50429): this falls back to the Scalar path for the whole
+            # array even when the type contains no maps; threading
+            # maps_as_pydicts through _getitem_py keeps the fast paths instead.
+            return [x.as_py(maps_as_pydicts=maps_as_pydicts) for x in self]
+        # TODO(GH-50448): convert per range instead of per element to cut
+        # the per-element call overhead further.
+        return [self._getitem_py(i) for i in range(n)]
+
+    cdef object _getitem_py(self, int64_t i):
+        # Return self[i] as a Python object, without creating a Python Scalar
+        # (nor, for nested types, per-row Array wrappers) where a subclass
+        # provides a specialization; this base implementation goes through
+        # Scalar.as_py and thus preserves its semantics exactly (see GH-50326).
+        return self.getitem(i).as_py()
 
     def tolist(self):
         """
@@ -2440,6 +2457,12 @@ cdef class BooleanArray(Array):
     """
     Concrete class for Arrow arrays of boolean data type.
     """
+
+    cdef object _getitem_py(self, int64_t i):
+        if self.ap.IsNull(i):
+            return None
+        return (<CBooleanArray*> self.ap).Value(i)
+
     @property
     def false_count(self):
         return (<CBooleanArray*> self.ap).false_count()
@@ -2454,6 +2477,34 @@ cdef class NumericArray(Array):
     A base class for Arrow numeric arrays.
     """
 
+    cdef object _getitem_py(self, int64_t i):
+        cdef Type tid = self.ap.type_id()
+        if self.ap.IsNull(i):
+            return None
+        if tid == _Type_INT8:
+            return (<CInt8Array*> self.ap).Value(i)
+        elif tid == _Type_INT16:
+            return (<CInt16Array*> self.ap).Value(i)
+        elif tid == _Type_INT32:
+            return (<CInt32Array*> self.ap).Value(i)
+        elif tid == _Type_INT64:
+            return (<CInt64Array*> self.ap).Value(i)
+        elif tid == _Type_UINT8:
+            return (<CUInt8Array*> self.ap).Value(i)
+        elif tid == _Type_UINT16:
+            return (<CUInt16Array*> self.ap).Value(i)
+        elif tid == _Type_UINT32:
+            return (<CUInt32Array*> self.ap).Value(i)
+        elif tid == _Type_UINT64:
+            return (<CUInt64Array*> self.ap).Value(i)
+        elif tid == _Type_FLOAT:
+            return (<CFloatArray*> self.ap).Value(i)
+        elif tid == _Type_DOUBLE:
+            return (<CDoubleArray*> self.ap).Value(i)
+        # Subclasses whose as_py returns non-primitive objects (dates, times,
+        # timestamps, durations, half floats, ...) use the exact Scalar path.
+        return Array._getitem_py(self, i)
+
 
 cdef class IntegerArray(NumericArray):
     """
@@ -2772,6 +2823,16 @@ cdef class ListArray(BaseListArray):
     Concrete class for Arrow arrays of a list data type.
     """
 
+    cdef object _getitem_py(self, int64_t i):
+        cdef CListArray* arr = <CListArray*> self.ap
+        if arr.IsNull(i):
+            return None
+        if self._children_cache is None:
+            self._children_cache = pyarrow_wrap_array(arr.values())
+        cdef Array values = <Array> self._children_cache
+        cdef int64_t j, start = arr.value_offset(i), end = arr.value_offset(i 
+ 1)
+        return [values._getitem_py(j) for j in range(start, end)]
+
     @staticmethod
     def from_arrays(offsets, values, DataType type=None, MemoryPool pool=None, 
mask=None):
         """
@@ -2957,6 +3018,16 @@ cdef class LargeListArray(BaseListArray):
     Identical to ListArray, but 64-bit offsets.
     """
 
+    cdef object _getitem_py(self, int64_t i):
+        cdef CLargeListArray* arr = <CLargeListArray*> self.ap
+        if arr.IsNull(i):
+            return None
+        if self._children_cache is None:
+            self._children_cache = pyarrow_wrap_array(arr.values())
+        cdef Array values = <Array> self._children_cache
+        cdef int64_t j, start = arr.value_offset(i), end = arr.value_offset(i 
+ 1)
+        return [values._getitem_py(j) for j in range(start, end)]
+
     @staticmethod
     def from_arrays(offsets, values, DataType type=None, MemoryPool pool=None, 
mask=None):
         """
@@ -3547,6 +3618,19 @@ cdef class MapArray(ListArray):
     Concrete class for Arrow arrays of a map data type.
     """
 
+    cdef object _getitem_py(self, int64_t i):
+        cdef CListArray* arr = <CListArray*> self.ap
+        if arr.IsNull(i):
+            return None
+        if self._children_cache is None:
+            self._children_cache = (self.keys, self.items)
+        cdef Array keys = <Array> (<tuple> self._children_cache)[0]
+        cdef Array items = <Array> (<tuple> self._children_cache)[1]
+        cdef int64_t j, start = arr.value_offset(i), end = arr.value_offset(i 
+ 1)
+        # Matches MapScalar.as_py with the default maps_as_pydicts=None:
+        # an association list of (key, value) tuples.
+        return [(keys._getitem_py(j), items._getitem_py(j)) for j in 
range(start, end)]
+
     @staticmethod
     def from_arrays(offsets, keys, items, DataType type=None, MemoryPool 
pool=None, mask=None):
         """
@@ -3684,6 +3768,16 @@ cdef class FixedSizeListArray(BaseListArray):
     Concrete class for Arrow arrays of a fixed size list data type.
     """
 
+    cdef object _getitem_py(self, int64_t i):
+        cdef CFixedSizeListArray* arr = <CFixedSizeListArray*> self.ap
+        if arr.IsNull(i):
+            return None
+        if self._children_cache is None:
+            self._children_cache = pyarrow_wrap_array(arr.values())
+        cdef Array values = <Array> self._children_cache
+        cdef int64_t j, start = arr.value_offset(i), end = arr.value_offset(i 
+ 1)
+        return [values._getitem_py(j) for j in range(start, end)]
+
     @staticmethod
     def from_arrays(values, list_size=None, DataType type=None, mask=None):
         """
@@ -3970,6 +4064,13 @@ cdef class StringArray(Array):
     Concrete class for Arrow arrays of string (or utf8) data type.
     """
 
+    cdef object _getitem_py(self, int64_t i):
+        if self.ap.IsNull(i):
+            return None
+        cdef cpp_string_view view = (<CBinaryArray*> self.ap).GetView(i)
+        # Matches StringScalar.as_py, which is str(buf, 'utf8').
+        return cp.PyUnicode_DecodeUTF8(view.data(), <Py_ssize_t> view.size(), 
NULL)
+
     @staticmethod
     def from_buffers(int length, Buffer value_offsets, Buffer data,
                      Buffer null_bitmap=None, int null_count=-1,
@@ -4002,6 +4103,12 @@ cdef class LargeStringArray(Array):
     Concrete class for Arrow arrays of large string (or utf8) data type.
     """
 
+    cdef object _getitem_py(self, int64_t i):
+        if self.ap.IsNull(i):
+            return None
+        cdef cpp_string_view view = (<CLargeBinaryArray*> self.ap).GetView(i)
+        return cp.PyUnicode_DecodeUTF8(view.data(), <Py_ssize_t> view.size(), 
NULL)
+
     @staticmethod
     def from_buffers(int length, Buffer value_offsets, Buffer data,
                      Buffer null_bitmap=None, int null_count=-1,
@@ -4034,11 +4141,24 @@ cdef class StringViewArray(Array):
     Concrete class for Arrow arrays of string (or utf8) view data type.
     """
 
+    cdef object _getitem_py(self, int64_t i):
+        if self.ap.IsNull(i):
+            return None
+        cdef cpp_string_view view = (<CBinaryViewArray*> self.ap).GetView(i)
+        return cp.PyUnicode_DecodeUTF8(view.data(), <Py_ssize_t> view.size(), 
NULL)
+
 
 cdef class BinaryArray(Array):
     """
     Concrete class for Arrow arrays of variable-sized binary data type.
     """
+
+    cdef object _getitem_py(self, int64_t i):
+        if self.ap.IsNull(i):
+            return None
+        cdef cpp_string_view view = (<CBinaryArray*> self.ap).GetView(i)
+        return cp.PyBytes_FromStringAndSize(view.data(), <Py_ssize_t> 
view.size())
+
     @property
     def total_values_length(self):
         """
@@ -4052,6 +4172,13 @@ cdef class LargeBinaryArray(Array):
     """
     Concrete class for Arrow arrays of large variable-sized binary data type.
     """
+
+    cdef object _getitem_py(self, int64_t i):
+        if self.ap.IsNull(i):
+            return None
+        cdef cpp_string_view view = (<CLargeBinaryArray*> self.ap).GetView(i)
+        return cp.PyBytes_FromStringAndSize(view.data(), <Py_ssize_t> 
view.size())
+
     @property
     def total_values_length(self):
         """
@@ -4066,6 +4193,12 @@ cdef class BinaryViewArray(Array):
     Concrete class for Arrow arrays of variable-sized binary view data type.
     """
 
+    cdef object _getitem_py(self, int64_t i):
+        if self.ap.IsNull(i):
+            return None
+        cdef cpp_string_view view = (<CBinaryViewArray*> self.ap).GetView(i)
+        return cp.PyBytes_FromStringAndSize(view.data(), <Py_ssize_t> 
view.size())
+
 
 cdef class DictionaryArray(Array):
     """
@@ -4225,6 +4358,28 @@ cdef class StructArray(Array):
     Concrete class for Arrow arrays of a struct data type.
     """
 
+    cdef object _getitem_py(self, int64_t i):
+        if self.ap.IsNull(i):
+            return None
+        cdef int64_t k, num_fields = self.type.num_fields
+        if self._children_cache is None:
+            names = [self.type.field(k).name for k in range(num_fields)]
+            if len(set(names)) != len(names):
+                # Matches StructScalar.as_py
+                raise ValueError(
+                    "Converting to Python dictionary is not supported when "
+                    "duplicate field names are present")
+            self._children_cache = (
+                names, [self.field(k) for k in range(num_fields)])
+        names = (<tuple> self._children_cache)[0]
+        fields = (<tuple> self._children_cache)[1]
+        cdef Array field_arr
+        result = {}
+        for k in range(num_fields):
+            field_arr = <Array> fields[k]
+            result[names[k]] = field_arr._getitem_py(i)
+        return result
+
     def field(self, index):
         """
         Retrieves the child array belonging to field.
diff --git a/python/pyarrow/includes/libarrow.pxd 
b/python/pyarrow/includes/libarrow.pxd
index 8b4786ecbf..e57c6d0d92 100644
--- a/python/pyarrow/includes/libarrow.pxd
+++ b/python/pyarrow/includes/libarrow.pxd
@@ -264,7 +264,7 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
 
         c_string Diff(const CArray& other)
         c_bool Equals(const CArray& arr)
-        c_bool IsNull(int i)
+        c_bool IsNull(int64_t i)
 
         shared_ptr[CArrayData] data()
 
@@ -675,87 +675,87 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
                         c_string* result)
 
     cdef cppclass CBooleanArray" arrow::BooleanArray"(CArray):
-        c_bool Value(int i)
+        c_bool Value(int64_t i)
         int64_t false_count()
         int64_t true_count()
 
     cdef cppclass CUInt8Array" arrow::UInt8Array"(CArray):
-        uint8_t Value(int i)
+        uint8_t Value(int64_t i)
 
     cdef cppclass CInt8Array" arrow::Int8Array"(CArray):
-        int8_t Value(int i)
+        int8_t Value(int64_t i)
 
     cdef cppclass CUInt16Array" arrow::UInt16Array"(CArray):
-        uint16_t Value(int i)
+        uint16_t Value(int64_t i)
 
     cdef cppclass CInt16Array" arrow::Int16Array"(CArray):
-        int16_t Value(int i)
+        int16_t Value(int64_t i)
 
     cdef cppclass CUInt32Array" arrow::UInt32Array"(CArray):
-        uint32_t Value(int i)
+        uint32_t Value(int64_t i)
 
     cdef cppclass CInt32Array" arrow::Int32Array"(CArray):
-        int32_t Value(int i)
+        int32_t Value(int64_t i)
 
     cdef cppclass CUInt64Array" arrow::UInt64Array"(CArray):
-        uint64_t Value(int i)
+        uint64_t Value(int64_t i)
 
     cdef cppclass CInt64Array" arrow::Int64Array"(CArray):
-        int64_t Value(int i)
+        int64_t Value(int64_t i)
 
     cdef cppclass CDate32Array" arrow::Date32Array"(CArray):
-        int32_t Value(int i)
+        int32_t Value(int64_t i)
 
     cdef cppclass CDate64Array" arrow::Date64Array"(CArray):
-        int64_t Value(int i)
+        int64_t Value(int64_t i)
 
     cdef cppclass CTime32Array" arrow::Time32Array"(CArray):
-        int32_t Value(int i)
+        int32_t Value(int64_t i)
 
     cdef cppclass CTime64Array" arrow::Time64Array"(CArray):
-        int64_t Value(int i)
+        int64_t Value(int64_t i)
 
     cdef cppclass CTimestampArray" arrow::TimestampArray"(CArray):
-        int64_t Value(int i)
+        int64_t Value(int64_t i)
 
     cdef cppclass CDurationArray" arrow::DurationArray"(CArray):
-        int64_t Value(int i)
+        int64_t Value(int64_t i)
 
     cdef cppclass CMonthDayNanoIntervalArray \
             "arrow::MonthDayNanoIntervalArray"(CArray):
         pass
 
     cdef cppclass CHalfFloatArray" arrow::HalfFloatArray"(CArray):
-        uint16_t Value(int i)
+        uint16_t Value(int64_t i)
 
     cdef cppclass CFloatArray" arrow::FloatArray"(CArray):
-        float Value(int i)
+        float Value(int64_t i)
 
     cdef cppclass CDoubleArray" arrow::DoubleArray"(CArray):
-        double Value(int i)
+        double Value(int64_t i)
 
     cdef cppclass CFixedSizeBinaryArray" arrow::FixedSizeBinaryArray"(CArray):
-        const uint8_t* GetValue(int i)
+        const uint8_t* GetValue(int64_t i)
 
     cdef cppclass CDecimal32Array" arrow::Decimal32Array"(
         CFixedSizeBinaryArray
     ):
-        c_string FormatValue(int i)
+        c_string FormatValue(int64_t i)
 
     cdef cppclass CDecimal64Array" arrow::Decimal64Array"(
         CFixedSizeBinaryArray
     ):
-        c_string FormatValue(int i)
+        c_string FormatValue(int64_t i)
 
     cdef cppclass CDecimal128Array" arrow::Decimal128Array"(
         CFixedSizeBinaryArray
     ):
-        c_string FormatValue(int i)
+        c_string FormatValue(int64_t i)
 
     cdef cppclass CDecimal256Array" arrow::Decimal256Array"(
         CFixedSizeBinaryArray
     ):
-        c_string FormatValue(int i)
+        c_string FormatValue(int64_t i)
 
     cdef cppclass CListArray" arrow::ListArray"(CArray):
         @staticmethod
@@ -776,8 +776,8 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
         )
 
         const int32_t* raw_value_offsets()
-        int32_t value_offset(int i)
-        int32_t value_length(int i)
+        int32_t value_offset(int64_t i)
+        int32_t value_length(int64_t i)
         shared_ptr[CArray] values()
         shared_ptr[CArray] offsets()
         shared_ptr[CDataType] value_type()
@@ -800,8 +800,8 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
             shared_ptr[CBuffer] null_bitmap
         )
 
-        int64_t value_offset(int i)
-        int64_t value_length(int i)
+        int64_t value_offset(int64_t i)
+        int64_t value_length(int64_t i)
         shared_ptr[CArray] values()
         shared_ptr[CArray] offsets()
         shared_ptr[CDataType] value_type()
@@ -819,8 +819,8 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
             shared_ptr[CDataType],
             shared_ptr[CBuffer] null_bitmap)
 
-        int64_t value_offset(int i)
-        int64_t value_length(int i)
+        int64_t value_offset(int64_t i)
+        int64_t value_length(int64_t i)
         shared_ptr[CArray] values()
         shared_ptr[CDataType] value_type()
 
@@ -850,8 +850,8 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
 
         const int32_t* raw_value_offsets()
         const int32_t* raw_value_sizes()
-        int32_t value_offset(int i)
-        int32_t value_length(int i)
+        int32_t value_offset(int64_t i)
+        int32_t value_length(int64_t i)
         shared_ptr[CArray] values()
         shared_ptr[CArray] offsets()
         shared_ptr[CArray] sizes()
@@ -881,8 +881,8 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
             CMemoryPool* pool
         )
 
-        int64_t value_offset(int i)
-        int64_t value_length(int i)
+        int64_t value_offset(int64_t i)
+        int64_t value_length(int64_t i)
         shared_ptr[CArray] values()
         shared_ptr[CArray] offsets()
         shared_ptr[CArray] sizes()
@@ -911,8 +911,8 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
         shared_ptr[CArray] keys()
         shared_ptr[CArray] items()
         CMapType* map_type()
-        int64_t value_offset(int i)
-        int64_t value_length(int i)
+        int64_t value_offset(int64_t i)
+        int64_t value_length(int64_t i)
         shared_ptr[CArray] values()
         shared_ptr[CDataType] value_type()
 
@@ -941,18 +941,20 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
             const vector[c_string]& field_names,
             const vector[int8_t]& type_codes)
 
-        int32_t value_offset(int i)
+        int32_t value_offset(int64_t i)
         shared_ptr[CBuffer] value_offsets()
 
     cdef cppclass CBinaryArray" arrow::BinaryArray"(CArray):
-        const uint8_t* GetValue(int i, int32_t* length)
+        const uint8_t* GetValue(int64_t i, int32_t* length)
+        cpp_string_view GetView(int64_t i)
         shared_ptr[CBuffer] value_data()
         int32_t value_offset(int64_t i)
         int32_t value_length(int64_t i)
         int32_t total_values_length()
 
     cdef cppclass CLargeBinaryArray" arrow::LargeBinaryArray"(CArray):
-        const uint8_t* GetValue(int i, int64_t* length)
+        const uint8_t* GetValue(int64_t i, int64_t* length)
+        cpp_string_view GetView(int64_t i)
         shared_ptr[CBuffer] value_data()
         int64_t value_offset(int64_t i)
         int64_t value_length(int64_t i)
@@ -965,7 +967,7 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
                      int64_t null_count,
                      int64_t offset)
 
-        c_string GetString(int i)
+        c_string GetString(int64_t i)
 
     cdef cppclass CLargeStringArray" arrow::LargeStringArray" \
             (CLargeBinaryArray):
@@ -975,7 +977,13 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
                           int64_t null_count,
                           int64_t offset)
 
-        c_string GetString(int i)
+        c_string GetString(int64_t i)
+
+    cdef cppclass CBinaryViewArray" arrow::BinaryViewArray"(CArray):
+        cpp_string_view GetView(int64_t i)
+
+    cdef cppclass CStringViewArray" arrow::StringViewArray"(CBinaryViewArray):
+        pass
 
     cdef cppclass CStructArray" arrow::StructArray"(CArray):
         CStructArray(shared_ptr[CDataType]& type, int64_t length,
diff --git a/python/pyarrow/lib.pxd b/python/pyarrow/lib.pxd
index 683faa7855..38f1ac69a8 100644
--- a/python/pyarrow/lib.pxd
+++ b/python/pyarrow/lib.pxd
@@ -288,8 +288,15 @@ cdef class Array(_PandasConvertible):
         # To allow Table to propagate metadata to pandas.Series
         object _name
 
+    cdef:
+        # Lazily wrapped child array(s) reused by _getitem_py (see GH-50326).
+        # Appended after the pre-existing attributes to keep their offsets
+        # stable for extensions compiled against an older pyarrow.
+        object _children_cache
+
     cdef void init(self, const shared_ptr[CArray]& sp_array) except *
     cdef getitem(self, int64_t i)
+    cdef object _getitem_py(self, int64_t i)
     cdef int64_t length(self)
     cdef void _assert_cpu(self) except *
 
diff --git a/python/pyarrow/tests/test_array.py 
b/python/pyarrow/tests/test_array.py
index adc3e097b5..c1e3f0128b 100644
--- a/python/pyarrow/tests/test_array.py
+++ b/python/pyarrow/tests/test_array.py
@@ -465,6 +465,64 @@ def test_array_getitem_numpy_scalars():
         assert arr[np.int32(idx)].as_py() == lst[idx]
 
 
+def test_to_pylist_bulk_paths():
+    # GH-50326: to_pylist converts through scalar-free _getitem_py
+    # specializations; the result must match the per-scalar conversion
+    # exactly.
+    arrays = [
+        pa.array([[1, None, 3], None, [], [4]], type=pa.list_(pa.int32())),
+        pa.array([["a", None], None, [], ["bcd", ""]],
+                 type=pa.list_(pa.string())),
+        pa.array([["a", None], None, [], ["bcd", ""]],
+                 type=pa.large_list(pa.large_string())),
+        pa.array([[1, None], None, [3, 4]], type=pa.list_(pa.int32(), 2)),
+        pa.array([[[1], [2, None]], None, [None, [3]]],
+                 type=pa.list_(pa.list_(pa.int32()))),
+        pa.array([[("k1", 1), ("k2", None)], None, []],
+                 type=pa.map_(pa.string(), pa.int32())),
+        pa.array(["a", None, "", "\N{GRINNING FACE} \N{SNOWMAN}"],
+                 type=pa.string()),
+        pa.array(["a", None, "", "\N{GRINNING FACE} \N{SNOWMAN}"],
+                 type=pa.large_string()),
+        pa.array([b"a\x00b", None, b"", b"\xff"], type=pa.binary()),
+        pa.array([b"a\x00b", None, b""], type=pa.large_binary()),
+        # View types store short values inline and long values out-of-line;
+        # cover both, plus NUL bytes and non-ASCII data.
+        pa.array(["a", None, "", "\N{GRINNING FACE} \N{SNOWMAN}",
+                  "long string exceeding the inline view size"],
+                 type=pa.string_view()),
+        pa.array([b"a\x00b", None, b"", b"\xff",
+                  b"long binary value exceeding the inline view size"],
+                 type=pa.binary_view()),
+        pa.array([[b"x", None, b"\x00y"], None, []],
+                 type=pa.list_(pa.binary())),
+        pa.array([1, None, -(2**62), 2**62], type=pa.int64()),
+        pa.array([0, None, 2**63 + 7], type=pa.uint64()),
+        pa.array([-128, 127, None], type=pa.int8()),
+        pa.array([1.5, None, -0.5], type=pa.float64()),
+        pa.array([1.5, None], type=pa.float32()),
+        pa.array([True, None, False], type=pa.bool_()),
+        pa.array([{"a": 1, "b": "x"}, None, {"a": None, "b": None}],
+                 type=pa.struct([("a", pa.int32()), ("b", pa.string())])),
+        pa.array([], type=pa.list_(pa.int32())),
+        pa.array([None, None], type=pa.list_(pa.string())),
+    ]
+    for arr in arrays:
+        for view in (arr, arr.slice(1), arr.slice(0, 2), arr.slice(2)):
+            assert view.to_pylist() == [x.as_py() for x in view]
+
+    # Values inside numeric lists must stay Python ints/None, never floats
+    result = pa.array([[1, None, 3]], type=pa.list_(pa.int32())).to_pylist()
+    assert result == [[1, None, 3]]
+    assert [type(x) for x in result[0]] == [int, type(None), int]
+
+    # Duplicate struct field names raise like StructScalar.as_py does
+    dup = pa.StructArray.from_arrays(
+        [pa.array([1, 2]), pa.array(["a", "b"])], names=["x", "x"])
+    with pytest.raises(ValueError, match="duplicate field names"):
+        dup.to_pylist()
+
+
 def test_array_slice():
     arr = pa.array(range(10))
 

Reply via email to