Column datatypes#

A scalar column holds one value per row, and H5Col defines four families for it: numeric columns, fixed-length strings, booleans, and categoricals. Each family maps to plain HDF5 datatypes, so files remain readable everywhere. What the convention adds is precise semantics on top. Variable-length values per row are the job of list columns, which have their own chapter.

Every family is declared through the same object, a ColumnSpec, whose fields cover identity (name, description), the datatype, storage (chunks, filters — see filters), and value semantics (fill_value, valid_min, valid_max, units, units_vocabulary).

Numeric columns#

The convention’s core numeric set covers: int8 through int64, their unsigned counterparts, float32, and float64. They can be declared in several flavors: np.float64, "float64", or np.dtype("<f8"):

ColumnSpec(name="t_air", dtype="float64", units="degC", valid_min=-90, valid_max=60)

Unless you pass fill_value=, the column receives the convention’s recommended per-dtype sentinel as its fill (the exact values are tabulated in missing values). A numeric dtype outside that set, e.g., float16, has no recommended sentinel, so it is accepted only with an explicit fill_value=. Creation raises a FillValueError without one. valid_min and valid_max are stored as column attributes for consumers, and they guard the fill: a fill value inside the declared valid range is rejected at creation with FillValueError.

Fixed-length strings#

A string column declares its byte size with FixedString:

ColumnSpec(name="station", dtype=FixedString(nbytes=8))

Values are UTF-8 encoded into exactly nbytes bytes of storage per row, padded with NULs. The budget counts bytes, not characters — a four-character string of non-ASCII text can need more than four bytes. The guarantee that matters is that a value whose encoding exceeds the budget raises OversizedStringError and names the row, instead of being silently truncated. If the data can outgrow the column, you find out at write time, not at analysis time.

Reading decodes back to Python strings (a NumPy object array). The default fill value for a string column is the empty string, so an empty value reads as missing by default; if empty strings are meaningful data in your model, account for that when choosing the column’s fill.

Why fixed-length rather than HDF5’s variable-length strings? Variable-length data is stored on the HDF5 file’s global heap, outside the dataset’s chunks. This means chunk compression never touches the actual characters, and reads scatter across the file. Fixed-length strings keep every byte inside the chunk pipeline, where filters and contiguous reads work as intended. When string lengths genuinely vary too much for a fixed length, a list column with string values provides variable-length text that still lives in filterable chunks.

Boolean columns#

HDF5 has no native boolean type, so the convention fixes one: an enumeration over signed 8-bit integers with exactly two members, FALSE = 0 and TRUE = 1. Declare it with bool_dtype():

ColumnSpec(name="qc_passed", dtype=bool_dtype())

Booleans read back as a NumPy boolean array. The implementation is strict in both directions: appending accepts Python/NumPy booleans or exact 0/1 integers (SchemaError otherwise), and reading a stored code other than 0 or 1 raises ConformanceError rather than guessing (NumPy would happily call every nonzero value true).

A boolean column declares no fill value because a boolean cannot be missing so it must be supplied in every append, and it may not declare valid_min or valid_max.

Categorical columns#

A categorical column stores small integer codes and keeps the label values in a labels dataset under the table’s CATEGORIES group. Declare one by listing its categories; the code dtype is chosen automatically — the smallest signed integer whose positive range covers the category count, so up to 127 labels cost one byte per row:

ColumnSpec(name="payment_type", categories=["Credit card", "Cash", "Dispute"])

Labels may be strings (stored as a fixed-length string dataset sized to the longest label) or numbers. The column dataset carries a CATEGORIES object reference to its labels dataset, so the association survives renames and moves.

The API works in labels, not codes. append() takes label values. An unknown label raises SchemaError, and None marks a missing row. Column.read returns labels, with None where the code is the fill. The raw codes remain available as Column.codes, the labels as Column.categories, and an optional ordered flag (for ordinal categories) round-trips through the spec and Column.ordered.

The default fill code is -1 for signed code dtypes (and the type maximum for unsigned), which cannot collide with a valid code as long as the code type leaves room for it. A fill that would collide, an unsigned code type fully saturated by its categories, is rejected at creation, and an explicit categorical fill must likewise lie outside [0, ncategories).

Categoricals earn their keep twice: in storage, where a repeated 20-byte label costs one byte per row, and in queries, where equality predicates compare labels (field("payment_type") == "Cash") and a bitmap index answers them exactly without touching the column data.