"""Which casts fail, and the length of the null-typed child in each result.
Run: python matrix.py (needs only pyarrow). Prints one line per case, ok or FAIL.
"""
import pyarrow as pa
import pyarrow.compute as pc
print("pyarrow", pa.__version__)
s = pa.struct([("a", pa.int64()), ("n", pa.null())])
s_int32 = pa.struct([("a", pa.int32()), ("n", pa.null())])
s_no_null = pa.struct([("a", pa.int64()), ("n", pa.int64())])
L = pa.list_
cases = [
# Lists, not sliced: fails when the list has fewer rows than structs
("list [[x]]: 1 row, 1 struct", pa.array([[{"a": 1}]], L(s)), L(s)),
("list [[x, x]]: 1 row, 2 structs", pa.array([[{"a": 1}, {"a": 2}]], L(s)), L(s)),
("list [[x], [x]]: 2 rows, 2 structs", pa.array([[{"a": 1}], [{"a": 2}]], L(s)), L(s)),
("list [None, [x, x]]: 2 rows, 2 structs", pa.array([None, [{"a": 1}, {"a": 2}]], L(s)), L(s)),
("list [[x, x], [x]]: 2 rows, 3 structs", pa.array([[{"a": 1}, {"a": 2}], [{"a": 3}]], L(s)), L(s)),
("large_list [[x, x]]", pa.array([[{"a": 1}, {"a": 2}]], pa.large_list(s)), pa.large_list(s)),
("list_view [[x, x]]", pa.array([[{"a": 1}, {"a": 2}]], pa.list_view(s)), pa.list_view(s)),
("list [[x, x]] with n: int64 (control)", pa.array([[{"a": 1}, {"a": 2}]], L(s_no_null)), L(s_no_null)),
# Casts to another type
("list [[x, x]] -> large_list<same struct>", pa.array([[{"a": 1}, {"a": 2}]], L(s)), pa.large_list(s)),
("list [[x, x]] -> list<struct<a: int32, n: null>>", pa.array([[{"a": 1}, {"a": 2}]], L(s)), L(s_int32)),
# Structs, not in a list
("struct, 2 rows, not sliced", pa.array([{"a": 0}, {"a": 1}], s), s),
("struct, 2 rows, slice(1)", pa.array([{"a": 0}, {"a": 1}], s).slice(1), s),
("struct, 2 rows, slice(1) -> struct<a: int32, n: null>", pa.array([{"a": 0}, {"a": 1}], s).slice(1), s_int32),
]
for label, arr, to in cases:
arr.validate(full=True)
out = pc.cast(arr, to)
try:
out.validate(full=True)
print(f"ok {label}")
except pa.ArrowInvalid as e:
print(f"FAIL {label}: input length {len(arr)}, {e}")
# Array.cast and ChunkedArray.cast give the same invalid result, also without raising
arr = pa.array([[{"a": 1}, {"a": 2}]], L(s))
for label, out in [
("Array.cast", arr.cast(arr.type)),
("ChunkedArray.cast", pa.chunked_array([arr]).cast(arr.type)),
]:
try:
out.validate(full=True)
print(f"ok {label} of list [[x, x]]")
except pa.ArrowInvalid as e:
print(f"FAIL {label} of list [[x, x]]: {e}")
Describe the bug, including details regarding any error messages, version, and platform.
I was developing a routine to 'compact' my parquet files down to some different row group sizes (etc) with PyArrow, and along the way used a
RecordBatch.castwhich failed. I got a coding agent (Claude Opus 5.5) to dig into this and bisect it down to a minimal repro and it identified the cause as follows.Repro files:
matrix.py
Details
repro.py
Details
repro_parquet.py
Details
Description:
Take a struct with a field of type
null, nested in a list holding more structs than the list hasrows, or sliced. Cast it to its own type, and the result is invalid. The cast does not raise: the
result's null-typed child comes back with the length of the array passed to
cast, not thestruct's own length. It fails
validate(), andto_pylist()on it can raise.Array.cast,ChunkedArray.castandpyarrow.compute.castreturn the invalidarray without raising.
RecordBatch.castraisesArrowInvalid, because it validates its result.Reproduced on pyarrow 25.0.1, the latest release on PyPI on 2026-09-29, on Linux x86_64 with
Python 3.11. A search of apache/arrow issues on 2026-09-29 found no report of it. The nearest,
GH-50515 ("Respect parent validity bitmap when
casting nested structs with non-nullable fields"), is a different struct-cast bug.
Minimal repro
repro.py, which needs only pyarrow:A sliced struct array, not in a list, fails the same way:
Output of
python repro.py(exits 1 while the bug is present):Which casts fail
matrix.pyvalidates each cast's result. Output on 25.0.1:cast:a list with more structs than rows (
list,large_listandlist_viewalike), or a slicedstruct. The error's first number is the input's length, the second the length the struct needs.
int64casts correctly.atoint32) or the list type(
listtolarge_list), gives a valid result. Only a cast to the array's own type fails.Through Parquet
repro_parquet.pyshows how the bug arises in ordinary use. It writes[None, [x, x]]to Parquet and reads it back in 1-row batches. The second batch is[[x, x]], alist with 1 row and 2 structs. So
RecordBatch.castto the file's own schema fails on it, althoughthe batch passes
validate(full=True):Read whole, the file has 2 rows and 2 structs, and the cast succeeds.
Component(s)
Python