Skip to main content
Version: Next

Data Types

The Python client uses PyArrow types for schema definitions:

PyArrow TypeFluss TypePython Type
pa.bool_()Booleanbool
pa.int8() / int16() / int32() / int64()TinyInt / SmallInt / Int / BigIntint
pa.float32() / float64()Float / Doublefloat
pa.string()Stringstr
pa.binary()Bytesbytes
pa.binary(n)Binary(n)bytes
pa.date32()Datedatetime.date
pa.time32("ms")Timedatetime.time
pa.timestamp("us")Timestamp (NTZ)datetime.datetime
pa.timestamp("us", tz="UTC")TimestampLTZdatetime.datetime
pa.decimal128(precision, scale)Decimaldecimal.Decimal
pa.list_(type)Arraylist
pa.map_(key_type, value_type)Maplist[(key, value)]
pa.struct([(name, type), ...])Rowdict

All Python native types (date, time, datetime, Decimal) work when appending rows via dicts.

Nullability

PyArrow field nullability is preserved when constructing Fluss schemas. By default, fields are nullable. Use nullable=False on pa.field() to create a NOT NULL column:

schema = pa.schema([
pa.field("id", pa.int32(), nullable=False),
pa.field("name", pa.string()), # nullable by default
])
fluss_schema = fluss.Schema(schema)
fluss_schema.get_column_types() # ["int NOT NULL", "string"]

Primary key columns are automatically forced NOT NULL regardless of the PyArrow field setting.

For nested types, element nullability is also preserved:

schema = pa.schema([
pa.field("tags", pa.list_(pa.field("item", pa.string(), nullable=False))),
])
fluss_schema = fluss.Schema(schema)
fluss_schema.get_column_types() # ["array<string NOT NULL>"]

Writing Data

Rows can be dicts, lists, or tuples:

from datetime import date, time, datetime
from decimal import Decimal

row = {
"user_id": 1,
"name": "Alice",
"active": True,
"score": 95.5,
"balance": Decimal("1234.56"),
"birth_date": date(1990, 3, 15),
"login_time": time(9, 30, 0),
"created_at": datetime(2024, 1, 1, 0, 0, 0),
"nickname": None, # null value
"tags": ["active", "premium"], # Array of strings
"scores": [10, None, 30], # Array with null values
}
handle = writer.append(row)

When a row is written as a dict, a nullable column may be omitted — it defaults to null. A non-nullable column (including primary keys) must be present, otherwise the write is rejected with a clear error. The same rule applies to the fields of a ROW value.

Lists and tuples must have values in column order:

row = [1, "Alice", True, 95.5, Decimal("1234.56"), date(1990, 3, 15), time(9, 30, 0), datetime(2024, 1, 1), None]
handle = writer.append(row)

Reading Data

records = await scanner.poll(timeout_ms=1000)
for record in records:
row = record.row # dict[str, Any]
print(row["user_id"]) # int
print(row["name"]) # str
print(row["balance"]) # decimal.Decimal
print(row["birth_date"]) # datetime.date
print(row["created_at"]) # datetime.datetime

if row["nickname"] is None:
print("nickname is null")

Complex Types (Array, Map, Row)

ARRAY, MAP, and ROW columns can be nested arbitrarily (for example array<map<string, row<...>>>). On read they materialize to native Python objects; on write they accept the shapes below:

Fluss typeRead-back valueWrite input accepted
ARRAY<T>listlist / tuple
MAP<K, V>list of (key, value) tuplesdict, or a sequence of (key, value) pairs
ROW<...>dict keyed by field namedict (by name) or list/tuple (by position)

The MAP read shape matches pyarrow's MapArray.to_pylist() (it preserves duplicate keys and ordering); ROW matches StructArray.to_pylist().

Arrays

schema = pa.schema([
pa.field("id", pa.int32()),
pa.field("tags", pa.list_(pa.string())),
pa.field("matrix", pa.list_(pa.list_(pa.int32()))), # nested
])
writer.append({"id": 1, "tags": ["a", "b"], "matrix": [[1, 2], [3, 4]]})

row = await lookuper.lookup({"id": 1})
row["tags"] # ["a", "b"]
row["matrix"] # [[1, 2], [3, 4]]

Maps

Use pa.map_(key_type, value_type). Write a dict or a list of (key, value) pairs; reads return a list of (key, value) tuples (wrap with dict(...) for keyed access). Map keys must be non-null.

schema = pa.schema([
pa.field("id", pa.int32()),
pa.field("attrs", pa.map_(pa.string(), pa.int32())),
])
writer.append({"id": 1, "attrs": {"a": 1, "b": None}}) # dict input
# or a sequence of pairs: {"id": 2, "attrs": [("a", 1), ("b", None)]}

row = await lookuper.lookup({"id": 1})
row["attrs"] # [("a", 1), ("b", None)]
dict(row["attrs"]) # {"a": 1, "b": None}

Rows

Use pa.struct([...]). Write a dict keyed by field name (or a list/tuple in field order); reads return a dict.

schema = pa.schema([
pa.field("id", pa.int32()),
pa.field("profile", pa.struct([("age", pa.int32()), ("city", pa.string())])),
])
writer.append({"id": 1, "profile": {"age": 30, "city": "NYC"}})

row = await lookuper.lookup({"id": 1})
row["profile"] # {"age": 30, "city": "NYC"}

Constraints

ARRAY, MAP, and ROW may be used as row values and nested inside one another, but not as primary-key or bucket-key columns — the server rejects complex key types.

Bulk (Arrow) reads

The per-row paths above (append/upsert and the record-based scanner's record.row dict, point lookup) materialize each value into a Python object. For high-throughput scans, prefer the Arrow path — a record-batch scanner's to_arrow() / poll_arrow() returns nested columns as native pyarrow ListArray / MapArray / StructArray with no per-element conversion.