Skip to content

How to store records

When several values belong together, such as an x and a y, you can keep them in one field as a record: a dataclass, a NamedTuple, a fixed-length tuple, a TypedDict, an attrs class or a msgspec.Struct. Each member can have any type a field could have on its own, so a text member needs its capacity too.

1. Declare the record and the field

Define the record class as usual, then use it as a field's type:

from dataclasses import dataclass
from typing import Annotated, NamedTuple, NotRequired, TypedDict

from sharedbox import Capacity, SharedBox, field


@dataclass(frozen=True)
class Position:
    x: float
    y: float


class Limits(NamedTuple):
    low: int
    high: int


class Settings(TypedDict):
    speed: float
    note: NotRequired[Annotated[str, Capacity(16)]]


class Stage(SharedBox, name="example-stage"):
    position: Position = Position(0.0, 0.0)
    limits: Limits = Limits(0, 100)
    settings: Settings = field(default_factory=lambda: {"speed": 1.0})

2. Write and read the whole record

When you assign a record, every member is written at once, so a reader sees all of the new values or none of them. Reading builds a new instance of the class:

stage = Stage()
stage.position = Position(1.5, -2.0)
print(stage.position)  # Position(x=1.5, y=-2.0)

Your constructor runs on every read

Each read calls the class's constructor, so __post_init__, attrs converters and validators run every time. Make sure running them a second time changes nothing: a converter that rounds a float, for example, must leave an already rounded float as it is, or a read won't match what you wrote.

3. Change one member

A record you read from the box is a copy, so changing it changes nothing in the box. To change one member, read the record, make a new one with the change, and assign it back:

stage.limits = stage.limits._replace(high=50)
print(stage.limits)  # Limits(low=0, high=50)

If you use a frozen dataclass or a NamedTuple, the copy is read-only, so forgetting to assign it back gives you an error instead of a silent no-op.

4. Leave out a TypedDict key

A key marked NotRequired can be left out, and it is left out again when you read the record back:

print(stage.settings)  # {'speed': 1.0}
stage.settings = {"speed": 2.0, "note": "slow down"}
print(stage.settings["note"])  # slow down

Assigning a mapping with a key the TypedDict doesn't declare, or without a required key, raises TypeError.

What a record cannot hold

A read has to rebuild the record from its stored members, so a record that can't be rebuilt that way is refused with TypeError as soon as you define the box class. That covers a dataclass or attrs field with init=False, an InitVar without a default, and a generic record class. A record also can't hold a reference field.

The whole script
"""The script of the guide "How to store records"."""

from dataclasses import dataclass
from typing import Annotated, NamedTuple, NotRequired, TypedDict

from sharedbox import Capacity, SharedBox, field


@dataclass(frozen=True)
class Position:
    x: float
    y: float


class Limits(NamedTuple):
    low: int
    high: int


class Settings(TypedDict):
    speed: float
    note: NotRequired[Annotated[str, Capacity(16)]]


class Stage(SharedBox, name="example-stage"):
    position: Position = Position(0.0, 0.0)
    limits: Limits = Limits(0, 100)
    settings: Settings = field(default_factory=lambda: {"speed": 1.0})



stage = Stage()
stage.position = Position(1.5, -2.0)
print(stage.position)  # Position(x=1.5, y=-2.0)

stage.limits = stage.limits._replace(high=50)
print(stage.limits)  # Limits(low=0, high=50)

print(stage.settings)  # {'speed': 1.0}
stage.settings = {"speed": 2.0, "note": "slow down"}
print(stage.settings["note"])  # slow down

stage.close()
Stage.unlink()