Skip to content

Field types

A field can hold most of the types you'd put in a dataclass. Each one is stored in the segment in a fixed number of bytes, which is why a value never needs more room than the field set aside when the box was created. Below, each type is shown with an example value and the exact bytes a box stores for it, lowest address first; | separates the parts of a value, and you can point at a table to read how its types are laid out:

What each type stores
numbers and textbool True01int 100A 00 00 00 00 00 00 00float 1.500 00 00 00 00 00 F8 3Fcomplex 1+2j1.0 as a float | 2.0 as a floatstr "été", capacity 805 00 00 00 | C3 A9 74 C3 A9 | 3 unusedbytes or bytearray b"abc"03 00 00 00 | 61 62 63 | unusedDecimal("1.50")04 00 00 00 | 31 2E 35 30, the textdates and timesdate 2026-10-0839 4A 0B 00, day 739897time 09:30, naive00 96 7A F6 07 00 00 00 | 00 00 | 01 | 5 zerosdatetime 2026-10-08 09:30 UTC00 D6 2B E0 50 5D 06 00 | 00 00 | 00 | 5 zerostimedelta 1 day, 30 s01 00 00 00 | 1E 00 00 00 | 00 00 00 00UUID 12345678-1234-...12 34 56 78 12 34 56 78 ..., 16 byteschoicesEnum member, 3rd02 00Literal value, 3rd02 00Flag or IntFlag A | C05 00 00 00 00 00 00 00int | None = 501 | 7 padding | 05 00 00 00 00 00 00 00int | None = None00 | 7 padding | 8 zerosint | str, a unionmember number | padding | that memberrecords, collections and arraysdataclass, NamedTuple, tuple, TypedDict, attrs, msgspeceach member at its offset, packed like fieldslist[int], capacity 3 = [7, 9]02 00 00 00 | 4 padding | 07 00 ... | 09 00 ... | 1 unused slotset, frozenset, tuple[T, ...]as a listdictas a list, with a key and value in each slotarray uint8, shape (2, 2)01 02 03 04, in C orderSharedBox subclasscreate id | schema hash | name, 144 bytesNumbers are little-endian. Text and bytes start with a u32 length, then take their whole capacity whatever they hold. A date is its day number, date.toordinal(). A time or datetime is microseconds, the UTC offset in minutes and flags (bit 0: naive, bit 1: fold), then 5 zero bytes. A UUID is its 16 bytes in network order. An enum member or a literal value is stored by its position, counting from 0. An optional or a union starts with a byte that says which member it holds, padded to the member's alignment. A record keeps its members at fixed offsets, like a small box. A collection starts with its length, padded to its slots' alignment, then has room for every element up to its capacity. A reference field stores which box it points at, never that box's values.
numbers and textbool True01int 100A 00 00 00 00 00 00 00float 1.500 00 00 00 00 00 F8 3Fcomplex 1+2j1.0 as a float | 2.0 as a floatstr "été", capacity 805 00 00 00 | C3 A9 74 C3 A9 | 3 unusedbytes or bytearray b"abc"03 00 00 00 | 61 62 63 | unusedDecimal("1.50")04 00 00 00 | 31 2E 35 30, the textdates and timesdate 2026-10-0839 4A 0B 00, day 739897time 09:30, naive00 96 7A F6 07 00 00 00 | 00 00 | 01 | 5 zerosdatetime 2026-10-08 09:30 UTC00 D6 2B E0 50 5D 06 00 | 00 00 | 00 | 5 zerostimedelta 1 day, 30 s01 00 00 00 | 1E 00 00 00 | 00 00 00 00UUID 12345678-1234-...12 34 56 78 12 34 56 78 ..., 16 byteschoicesEnum member, 3rd02 00Literal value, 3rd02 00Flag or IntFlag A | C05 00 00 00 00 00 00 00int | None = 501 | 7 padding | 05 00 00 00 00 00 00 00int | None = None00 | 7 padding | 8 zerosint | str, a unionmember number | padding | that memberrecords, collections and arraysdataclass, NamedTuple, tuple, TypedDict, attrs, msgspeceach member at its offset, packed like fieldslist[int], capacity 3 = [7, 9]02 00 00 00 | 4 padding | 07 00 ... | 09 00 ... | 1 unused slotset, frozenset, tuple[T, ...]as a listdictas a list, with a key and value in each slotarray uint8, shape (2, 2)01 02 03 04, in C orderSharedBox subclasscreate id | schema hash | name, 144 bytesNumbers are little-endian. Text and bytes start with a u32 length, then take their whole capacity whatever they hold. A date is its day number, date.toordinal(). A time or datetime is microseconds, the UTC offset in minutes and flags (bit 0: naive, bit 1: fold), then 5 zero bytes. A UUID is its 16 bytes in network order. An enum member or a literal value is stored by its position, counting from 0. An optional or a union starts with a byte that says which member it holds, padded to the member's alignment. A record keeps its members at fixed offsets, like a small box. A collection starts with its length, padded to its slots' alignment, then has room for every element up to its capacity. A reference field stores which box it points at, never that box's values.

NewType, Final, Annotated and type aliases work too: the box looks through them to the type they stand for.

What the segment says about a type

For a plain int, the kind alone says how the bytes are laid out. For an enum, a record or a list it doesn't, so the segment also keeps a description of the type: its members, their names, a capacity or a shape. That's what lets a C or C++ program read a field of any type; the segment layout spells it out.

The schema hash covers these descriptions, so if an enum gains a member or a record member changes type, the new class no longer attaches to boxes made with the old one. It doesn't cover choices that only affect what Python builds when it reads a value back: a record class's name, list or tuple[T, ...], set or frozenset.

A read builds a new value

Every read turns the stored bytes into a new object. So if you change a list, a record or an array you read from a box, you only change that object; to store the change, assign it back. A record is rebuilt by calling its class, which means its __post_init__, converters and validators run on every read.

Dates and times

A datetime or time is either naive or keeps a fixed offset from UTC. If yours has a time zone, the box stores its offset at that moment, in whole minutes, and gives it back with a datetime.timezone of that offset, so a named zone such as a ZoneInfo comes back as a plain offset. A time needs a tzinfo that can give an offset without a date. A date field refuses a datetime, since storing it would lose the time of day.

Enums and literals

An enum member is stored by its position in the class, so reading gives you back the member itself and is comparisons work. Reordering the members changes what the stored positions mean, so the schema hash changes with it and old boxes won't attach. In a Literal field, values of different types count as different values even when Python would call them equal: in Literal[1, True], 1 and True are two separate values, and each reads back as itself.

Unions

A union stores which of its members it holds, so a value comes back as the type you wrote. A value whose type is exactly one member's type goes to that member; otherwise the most specific member that takes it wins, so bool comes before int, and float | int keeps 3 an int. Two members that take values of the same class, such as list[int] | list[str], are refused with TypeError when you define the class, because the box couldn't tell which one a stored value belongs to.

When a field counts as changed

events and watch compare stored bytes, not Python values, to decide whether a field changed. That gives a few surprises: writing NaN again reports nothing, while -0.0 after 0.0 reports a change, and so does True after 1 in an int | bool field.

Large values

A box has one write lock, so while a large array is copied in or out, every other read and write of that box waits. The copy itself runs with the GIL released, so your other threads keep going. Keep a large array in a box of its own, so small fields don't wait behind it. How fast a box is gives the cost of the copy, and How to store arrays shows how to read into an array you already have.