You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by fo...@apache.org on 2026/07/19 06:01:19 UTC
(iceberg-python) branch main updated: feat: support pyarrow float16 by widening to float on read/write (#3590)
This is an automated email from the ASF dual-hosted git repository.
Fokko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git
The following commit(s) were added to refs/heads/main by this push:
new 4223f4cb1 feat: support pyarrow float16 by widening to float on read/write (#3590)
4223f4cb1 is described below
commit 4223f4cb187e796befd61c2536e31ada4a9aa631
Author: Anas Khan <an...@gmail.com>
AuthorDate: Sun Jul 19 11:31:13 2026 +0530
feat: support pyarrow float16 by widening to float on read/write (#3590)
# Rationale for this change
PyArrow `float16` (`halffloat`) currently raises
`UnsupportedPyArrowTypeException` during schema conversion, because
`_ConvertToIceberg.primitive` only handles `float32` and `float64`:
```python
>>> import pyarrow as pa
>>> from pyiceberg.io.pyarrow import _ConvertToIceberg, visit_pyarrow
>>> visit_pyarrow(pa.float16(), _ConvertToIceberg())
pyiceberg.exceptions.UnsupportedPyArrowTypeException: Column 'x' has an unsupported type: halffloat
```
Iceberg has no half-precision float, but `float16 -> float32` is
**lossless**: every IEEE 754 half value (including the maximum finite
value 65504) is exactly representable in single precision. This mirrors
how the same method already widens `int8`/`int16` to `IntegerType`, and
how `ArrowProjectionVisitor._cast_if_needed` already widens smaller
integers up for cross-platform compatibility. Mapping `float16 ->
FloatType` is the float analogue, so `float16` columns round-trip
instead of erroring.
Changes (`pyiceberg/io/pyarrow.py`):
- `_ConvertToIceberg.primitive`: map `pa.float16()` -> `FloatType()`.
- `ArrowProjectionVisitor._cast_if_needed`: widen smaller float types to
the target type on write (parallel to the existing integer-widening
branch), so `float16` arrays are cast to `float32`. Narrowing falls
through to the existing `promote()` handling.
No dependency changes; `pyproject.toml` / `uv.lock` are untouched and
the imports used were already present.
A note on a design choice, deferring to maintainers: widening `float16`
silently (rather than erroring or gating behind a config flag) follows
the existing `int8`/`int16 -> Integer` precedent. Happy to gate it
behind a config option instead if you'd prefer. The new float-widening
branch also makes `float32 -> DoubleType` actually cast the array
(parallel to int widening), so it slightly tightens float promotion in
general, not just `float16`.
## Are these changes tested?
Yes:
- `tests/io/test_pyarrow_visitor.py::test_pyarrow_float16_to_iceberg`
asserts the schema mapping `pa.float16() -> FloatType()`.
- `tests/io/test_pyarrow.py::test__to_requested_schema_float_promotion`
is parametrized over `f16 -> Float`, `f16 -> Double`, and `f32 ->
Double`, asserting both the written PyArrow type and that the values are
preserved.
Both pass locally, the surrounding visitor suite and the sibling
integer-promotion test still pass, and `make lint` (ruff, ruff-format,
mypy, pydocstyle, codespell, uv-lock) is clean. The integration suite
(Docker/Spark) was not run locally.
## Are there any user-facing changes?
Yes. PyArrow tables with `float16` columns can now be converted/written
through PyIceberg (they map to Iceberg `float` and are stored as
`float32`), where they previously raised
`UnsupportedPyArrowTypeException`. This is purely additive; existing
`float32`/`float64` behavior is unchanged.
---
pyiceberg/io/pyarrow.py | 12 ++++++++++++
tests/io/test_pyarrow.py | 29 +++++++++++++++++++++++++++++
tests/io/test_pyarrow_visitor.py | 6 ++++++
3 files changed, 47 insertions(+)
diff --git a/pyiceberg/io/pyarrow.py b/pyiceberg/io/pyarrow.py
index 7e1babd58..f05781d9b 100644
--- a/pyiceberg/io/pyarrow.py
+++ b/pyiceberg/io/pyarrow.py
@@ -1438,6 +1438,9 @@ class _ConvertToIceberg(PyArrowSchemaVisitor[IcebergType | Schema]):
else:
# Does not exist (yet)
raise TypeError(f"Unsupported integer type: {primitive}")
+ elif pa.types.is_float16(primitive):
+ # Iceberg has no half-precision float; widen to single precision (lossless)
+ return FloatType()
elif pa.types.is_float32(primitive):
return FloatType()
elif pa.types.is_float64(primitive):
@@ -1979,6 +1982,15 @@ class ArrowProjectionVisitor(SchemaWithPartnerVisitor[pa.Array, pa.Array | None]
target_width = target_type.bit_width
if source_width < target_width:
return values.cast(target_type)
+ elif isinstance(field.field_type, (FloatType, DoubleType)):
+ # Cast smaller float types to target type for cross-platform compatibility
+ # Only allow widening conversions (smaller bit width to larger), e.g. float16 -> float32
+ # Narrowing conversions fall through to promote() handling below
+ if pa.types.is_floating(values.type):
+ source_width = values.type.bit_width
+ target_width = target_type.bit_width
+ if source_width < target_width:
+ return values.cast(target_type)
if field.field_type != file_field.field_type:
target_schema = schema_to_pyarrow(
diff --git a/tests/io/test_pyarrow.py b/tests/io/test_pyarrow.py
index 532311899..dde354735 100644
--- a/tests/io/test_pyarrow.py
+++ b/tests/io/test_pyarrow.py
@@ -3109,6 +3109,35 @@ def test__to_requested_schema_integer_promotion(
assert result.column(0).to_pylist() == [1, 2, 3, None]
+@pytest.mark.parametrize(
+ "arrow_type,iceberg_type,expected_arrow_type",
+ [
+ (pa.float16(), FloatType(), pa.float32()),
+ (pa.float16(), DoubleType(), pa.float64()),
+ (pa.float32(), DoubleType(), pa.float64()),
+ ],
+)
+def test__to_requested_schema_float_promotion(
+ arrow_type: pa.DataType,
+ iceberg_type: PrimitiveType,
+ expected_arrow_type: pa.DataType,
+) -> None:
+ """Test that smaller float types are cast to target Iceberg type during write."""
+ requested_schema = Schema(NestedField(1, "col", iceberg_type, required=False))
+ file_schema = requested_schema
+
+ arrow_schema = pa.schema([pa.field("col", arrow_type)])
+ data = pa.array([1.5, 2.25, 3.0, None], type=arrow_type)
+ batch = pa.RecordBatch.from_arrays([data], schema=arrow_schema)
+
+ result = _to_requested_schema(
+ requested_schema, file_schema, batch, downcast_ns_timestamp_to_us=False, include_field_ids=False
+ )
+
+ assert result.schema[0].type == expected_arrow_type
+ assert result.column(0).to_pylist() == [1.5, 2.25, 3.0, None]
+
+
def test_pyarrow_file_io_fs_by_scheme_cache() -> None:
# It's better to set up multi-region minio servers for an integration test once `endpoint_url` argument
# becomes available for `resolve_s3_region`
diff --git a/tests/io/test_pyarrow_visitor.py b/tests/io/test_pyarrow_visitor.py
index 6727b8c76..e98d76e26 100644
--- a/tests/io/test_pyarrow_visitor.py
+++ b/tests/io/test_pyarrow_visitor.py
@@ -128,6 +128,12 @@ def test_pyarrow_int64_to_iceberg() -> None:
assert visit(converted_iceberg_type, _ConvertToArrowSchema()) == pyarrow_type
+def test_pyarrow_float16_to_iceberg() -> None:
+ pyarrow_type = pa.float16()
+ converted_iceberg_type = visit_pyarrow(pyarrow_type, _ConvertToIceberg())
+ assert converted_iceberg_type == FloatType()
+
+
def test_pyarrow_float32_to_iceberg() -> None:
pyarrow_type = pa.float32()
converted_iceberg_type = visit_pyarrow(pyarrow_type, _ConvertToIceberg())