Arrays are the core data structure in marrow. They are immutable and columnar — values sit contiguously in memory with a separate validity bitmap tracking null positions. Immutability is what makes a copy O(1): every array holds its data behind ref-counted buffers, so sharing one costs a refcount bump, never a memcpy.
Null is not zero.is_valid() reports False at index 1 — the value there is absent entirely, not 0 or empty. Array.is_valid() returns an elementwise BoolArray (like pyarrow.compute.is_valid); index into the array first (arr[1]) to check a single position.
FixedSizeList arrays store sequences of a fixed length per element — the natural layout for embedding vectors or coordinates, where every row has the same width.
Type inference always produces a variable-length list, even when every row happens to be the same length:
# inference sees lists — the result is list<float64>, not a fixed-size listcoords = ma.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])print(coords.type) # list<float64>
list<float64>
To get a genuine fixed_size_list, pass the type explicitly with fixed_size_list_(value_type, size). The fixed width is part of the type, which lets kernels skip per-row offset lookups:
ma.array() is a convenience over the native builder API — the mutable staging area you accumulate values in before freezing them with finish(). Reach for it in Mojo when values arrive one at a time.
from marrow.builders importInt64Builderfrom marrow.arrays importInt64Arrayvar b =Int64Builder(capacity=4)b.append(10)b.append(20)b.append_null()b.append(40)var arr:Int64Array= b.finish()print(arr) # PrimitiveArray[int64]([10, 20, NULL, 40])
capacity pre-allocates the backing buffer; it is optional but avoids reallocation when the size is known.
A ListBuilder wraps a child builder. Append child values, then commit each list element with append_valid() — or append_null() for a null list:
from marrow.builders importListBuilder, Int64Buildervar lists =ListBuilder(Int64Builder())var child_any = lists.values() # a DynBuilder over the childref child = child_any.as_int64() # borrow it at its real typechild.append(1)child.append(2)lists.append_valid() # [1, 2]child.append(3)lists.append_valid() # [3]lists.append_null() # a null listvar arr = lists.finish()
values() hands back a DynBuilder, which has no append of its own — narrow it with .as_int64() / .as_string() / … first. StructBuilder and FixedSizeListBuilder follow the same build-children-then-commit shape.
Type-erased builders
DynBuilder is the runtime-typed builder. Construct one from a DataType when the type is only known at run time; typed builders convert to it implicitly:
from marrow.builders importDynBuilder, Int64Builderfrom marrow.dtypes import int64var dyn =DynBuilder(int64) # int64 is already an instance — not int64()var typed =Int64Builder()var erased:DynBuilder= typed^# move it in -- no explicit wrapper needed
The conversion is implicit, so no DynBuilder(...) wrapper is needed, but the typed builder is moved (typed^) rather than copied: builders are not ImplicitlyCopyable, which keeps ownership visible at the call site.
finish() on a DynBuilder returns a DynArray, the type-erased array container.