Skip to content

Boxes

Name What it does
SharedBox a record whose annotated fields live in a named shared-memory segment
field sets the options of one field of a SharedBox class, as dataclasses.field does
fields returns one Field per field of a SharedBox subclass or box, in declaration order
Field options of one field of a SharedBox class, as field takes them
Capacity maximum size of a field: bytes for text and bytes, elements for a collection. See capacity

SharedBox

SharedBox(*args: Any, **kwargs: Any)

A record whose annotated fields live in a named shared-memory segment.

Subclass it and annotate the fields. Calling the subclass with the field values, as with a dataclass, creates the segment under the class's name and writes the values; create does the same under another name, so one class can describe several boxes, and attach opens an existing box from any thread or process. Every process that opens the segment reads and writes the same values.

A field is a public annotation with one of these types:

Annotation Stored as
bool, int, float, complex fixed-size numbers; a float field takes an int
Annotated[str, Capacity(n)], bytes, bytearray, Decimal at most n bytes
date, time, datetime, timedelta, UUID fixed-size values; times naive or with a fixed offset
an Enum, Flag or Literal the member's position, the bits, or the value's position
an optional X (Optional[X]), a union the member the value's type picks
a dataclass, NamedTuple, tuple, TypedDict, attrs class or msgspec.Struct its members
Annotated[list[T], Capacity(n)], and set, frozenset, dict, tuple[T, ...] at most n elements
Annotated[<array type>, Shape(...), DType(...)] the array's elements

A SharedBox subclass, optional or not, makes a reference field, described below. Capacity sets n. A read returns a new value: changing a list, record or array read from a box changes only that copy. Names starting with _ and ClassVar annotations are not fields. Fields of a base class come first.

Fields are positional by default, in declaration order, as in a dataclass. Fields declared after a dataclasses.KW_ONLY annotation in the same class are keyword-only. A default is a plain class attribute or given with field, and is checked against the field's type and capacity when the class is defined. inspect.signature of the class gives its constructor's parameters; a default_factory default shows as <factory>.

Assigning a value of the wrong type raises TypeError, and a value that does not fit its field raises ValueError, or OverflowError for an int value out of range for its int, float or complex field; either way the stored value does not change. A bytes field takes bytes, bytearray and memoryview values. Assigning to a name that is not a field raises AttributeError, and so does deleting a field. A box has no __dict__: every subclass gets empty __slots__ unless it declares its own. Two boxes are equal only if they are the same object.

A dataclasses.InitVar[T] annotation declares a constructor argument that is not stored. It takes a position like a field and may have a default, as a class attribute or with field(default=...), but no default_factory and no init=False. It is not in the layout, the schema hash, fields, snapshot or events. An InitVar named like an inherited field removes that field from the subclass, as in dataclasses; reading or assigning that name on a box raises AttributeError.

__post_init__(self, *initvars) runs when the class is called and in create, after the values are written, with each InitVar value in declaration order. It may read and assign fields, reference fields included, and its writes go into the segment. It does not run for attach or unpickling. The box is published when __post_init__ returns, so no other process sees it before then. Until then its name is taken: creating another box under it raises SegmentExistsError, and attach, from any process or from __post_init__ itself, waits at most 1 s (the lock timeout, if shorter) and then raises SegmentNotFoundError. __sharedbox_box__ raises BufferError until then too. If __post_init__ raises, the box is closed and its name removed without ever being published, and the exception propagates.

The class statement takes these keywords:

  • name: the segment name of boxes made by calling the class. By default it is 16 hex digits of SHA-256 over the class's identity, so every process that imports the class uses the same name. It must match [A-Za-z0-9_.-]{1,128}.
  • kw_only: make every field this class declares keyword-only. Inherited fields keep the setting of the class that declares them.
  • lock_timeout: seconds a read or write waits for a write in progress before it raises LockTimeoutError; 5.0 by default, finite and in (0, 86400].
  • identity: a non-empty string, by default the class's module.qualname, with __mp_main__ read as __main__. It enters the schema hash, and names the box when name is not given. A subclass does not inherit it. Two classes with the same identity and fields share a box, even when they live in different modules; a process whose class has another identity cannot attach it.
  • max_waiters: how many waiter slots the box has, 1 to 4096, 64 by default. Each box handle whose watch or events is in use holds one slot, counted across every process. A watcher that finds every slot taken logs a warning to the sharedbox logger and checks for changes once a second until a slot is free.

A field annotated with a SharedBox subclass X, or with X | None (or Optional[X]), refers to another box, which keeps its own segment, lock and lifetime. The field stores the box's name, schema hash and create id. X is the class being defined or a class defined before it. A field X is never empty: assigning None raises TypeError, and reading it returns a box. A field X | None may hold None. The two give different schema hashes, so a class that declares one cannot attach a box created by a class that declares the other. Defaults work as for any field: = None, field(default=box) and field(default_factory=...). A default box is checked like an assigned one when the class is defined, a factory's result at each creation. The class keeps the handle given as field(default=box) for as long as the class exists; once that handle is closed, each creation that uses the default raises BoxClosedError.

Assigning a box to a reference field stores which box it is, under the outer box's lock. A box of X, of a subclass of X, or of any class with X's schema hash (the same identity and fields) is accepted; any other value raises TypeError, and a closed box raises BoxClosedError. The constructor and update check reference values the same way. Type checkers accept a box of X or of a subclass; a box of another class with X's schema hash needs a cast.

Reading a reference field attaches the box with its own class, which may be a subclass of X, and keeps that handle: later reads return the same object while the field refers to the same box. The class is found by the stored schema hash among the SharedBox classes this process has defined; of several with one hash, the first one defined that is still alive is used. After another thread or process assigns another box, the next read attaches that one and closes the handle it kept. close on the outer box closes the handles its reads attached, and closing a returned box makes the next read attach it again. A read raises UnknownBoxClassError when this process has not imported the module that defines the box's class, and BrokenReferenceError when the box was removed, or removed and created again under the same name (also by another class), since it was assigned. A handle that already read the field keeps its own mapping of the box and goes on returning it. The outer box only points at the other box: closing or unlinking the outer box leaves it alone, and no write covers both boxes at once.

A box can be pickled, and copy.copy and copy.deepcopy do the same as a pickle round trip. The pickle holds the class, the segment name, the schema hash and the box's create id, a random number drawn when the box was created. Unpickling attaches a new handle: an independent box on the same data, which the receiving process closes. It reads reference fields from the segment like any other handle. A box pickled inside __post_init__ can be unpickled only once it is published. A child process started with fork uses the parent's box object, which keeps working after the fork.

Raises:

Type Description
TypeError

When the class is defined, for an annotation of another type, no fields or more than 256, a field named like a SharedBox member (name, closed, close, unlink, update, snapshot, watch, events, force_unlock, create, attach) or like follow, unfollow or nested, a default of the wrong type, a positional field without a default after one with a default, an InitVar in a class without __post_init__, a reference to a class that is not defined yet, __slots__ that name _segment, or an identity that is not a non-empty string. When the class is called, for a missing, unknown or repeated value, or a value of the wrong type.

ValueError

When the class is defined, for a name that does not match [A-Za-z0-9_.-]{1,128}, a lock_timeout or max_waiters out of range, or a default that does not fit its field (any of the cases below). When the class is called or a field is assigned, for a str, bytes or Decimal value longer than its capacity, a collection with more elements than its capacity, a tuple of the wrong length, an array of the wrong shape, a Literal field given another value, flag bits outside 0 to 2**64 - 1, a time or datetime whose UTC offset is not whole minutes of less than a day, or a time whose tzinfo gives no offset without a date.

OverflowError

When the class is defined or called, or a field is assigned, with an int value out of range for its int, float or complex field.

SegmentExistsError

When the class is called and the name is taken.

SchemaMismatchError

When a pickled box is loaded by a process whose class has different fields, or the box under that name was unlinked and created again since the pickle was made.

SegmentNotFoundError

When a pickled box is loaded after its segment is gone.

Notes

On Windows a lock wait ends on a timer tick, so lock_timeout can expire up to one tick late, 15.6 ms at the default timer resolution. The handle a class keeps for a field(default=box) default keeps that box's segment in existence on Windows, and so does the mapping a handle keeps of a box it read through a reference field: other handles can still attach it.

name property

name: str

Name of the segment; pass it to attach in another process.

closed property

closed: bool

True once close was called on this box.

events property

events: BoxEvents

One psygnal signal per field, emitted as (new, old) when any thread or process changes it.

A BoxEvents group: box.events.<field>.connect(cb) listens to one field and box.events.connect(cb) to all of them.

Unlike the callbacks of a local evented dataclass, these run on the box's watcher thread; connect with thread="main" and call psygnal.emit_queued() to run them on the main thread instead. Closing the box delivers writes the watcher thread had not seen yet, so callbacks may run once on the thread that calls close. A box that is garbage collected drops them.

If several writes happen between two checks by the watcher thread, only one emission happens, with the latest value; old is the value from the last emission. A write that leaves the value unchanged emits nothing. For the first emission of a field, old is the value the field held when events was first accessed.

A callback that raises is logged to the sharedbox logger. Callbacks connected before it on the same signal already ran; callbacks connected after it do not run for that write. Other fields still emit. The watcher thread also serves this box's watch iterators, so a slow callback delays them. A callback that refers to the box, such as a lambda that reads a field, keeps the box alive until close.

A field named like an attribute of psygnal's SignalGroup (connect, disconnect, all, signals, block and others) makes psygnal warn when the class is defined, and box.events.<name> then returns that attribute; box.events["<name>"] returns the field's signal.

psygnal's blocked, paused, throttled, debounced and psygnal.qt.start_emitting_from_queue work on these signals. A signal that is blocked drops its emissions in this process, and changes made while it is blocked are not emitted later.

A reference field's signal is emitted when the field is assigned another box or emptied, with a BoxRef or None as new and old. Changes inside the box it refers to are emitted by that box's own events, and by this group once follow forwards them.

A child process created with fork while a callback is running inherits that signal's lock as held, and hangs the first time it connects to, disconnects from or emits that signal.

unlink = Unlink()

Remove the segment's name, so no later process can attach to it.

Motor.unlink(name=None) on the class removes name, by default the class's name; box.unlink() on a box removes the name of its segment. Boxes already open keep working. On Linux, later attach calls fail. On Windows this does nothing; the segment goes away with its last handle.

Raises:

Type Description
ValueError

If name does not match [A-Za-z0-9_.-]{1,128}.

SegmentNotFoundError

On Linux, if no segment has that name.

create classmethod

create(name: str, /, *args: Any, **kwargs: Any) -> Self

Create a box under name instead of the class's name.

Takes the field values and runs __post_init__ as calling the class does.

Raises:

Type Description
SegmentExistsError

If the name is taken.

ValueError

If name does not match [A-Za-z0-9_.-]{1,128}.

attach classmethod

attach(name: str | None = None) -> Self

Open the box called name, by default the one named after this class.

__post_init__ does not run.

Raises:

Type Description
SegmentNotFoundError

If no segment has that name, or the shared memory under it does not become a box within 1 s (the lock timeout, if shorter).

SchemaMismatchError

If the segment was created by a different class or a different version of this class, uses another major version of the segment layout, or has a field of a kind this version cannot read.

ValueError

If name does not match [A-Za-z0-9_.-]{1,128}.

update

update(**values: Any) -> None

Write several fields under one lock; a reader sees all of the new values or none.

Every value is checked before any is written, so an update that raises changes nothing. A reference field takes a box or None, checked as when it is assigned.

Raises:

Type Description
TypeError

If a name is not a field or a value has the wrong type.

ValueError

If a str or bytes value is longer than its capacity.

snapshot

snapshot(*, follow: bool = False) -> dict[str, Any]

Return every field's value, with this box read at one point in time.

A reference field gives a BoxRef, or None when it is empty.

Parameters:

Name Type Description Default
follow bool

Replace each reference with the snapshot of the box it refers to, itself taken with follow. Each box is read at its own moment, not together with the others. A box this call has already read stays a BoxRef, also when a second field refers to it, so a loop of references (a box that refers to itself, or A to B and B to A) ends.

False

Raises:

Type Description
BrokenReferenceError

With follow, if a box referred to no longer exists or was created again.

UnknownBoxClassError

With follow, if no class defined in this process has the schema hash of a box referred to.

read_into

read_into(field: str, out: A) -> A

Copy an array field into out and return out.

The copy holds one complete write, as a read does, but goes into memory the caller already has, so nothing is allocated.

Parameters:

Name Type Description Default
out A

A writable, C-contiguous array in CPU memory with the field's DType and Shape. Its contents are unspecified if the call raises.

required

Raises:

Type Description
TypeError

If the field is not an array field, or out is not a writable, C-contiguous array in CPU memory.

ValueError

If the box has no such field, or out has another dtype or shape.

LockTimeoutError

If another writer holds the lock longer than the lock timeout.

BoxClosedError

If the box is closed.

writing

writing(field: str) -> Generator[Any, None, None]

Hold the box's write lock and yield the array field to fill in place.

The block gets an array of the field's declared type that views the field in shared memory and holds its current value. Leaving the block releases the lock, counts the write and wakes watchers, also when the block raises, since the bytes have already changed. While the block runs, reads and writes of the box in every process wait for the lock, so keep it short. Writing a field of this box inside the block waits on that same lock and raises LockTimeoutError. The array keeps its own mapping, so using it after the box is closed cannot crash, but writing to it after the block takes no lock and tells no one.

Raises:

Type Description
TypeError

If the field is not an array field.

ValueError

If the box has no such field.

LockTimeoutError

If another writer holds the lock longer than the lock timeout.

BoxClosedError

If the box is closed.

watch

watch(field: str) -> FieldWatch[Any]

Return a FieldWatch over the values written to field from now on.

A reference field yields a BoxRef or None.

Raises:

Type Description
ValueError

If the class has no field field.

force_unlock

force_unlock() -> None

Release a write lock left behind by a process that died while writing.

Reads and writes that wait on such a lock raise LockTimeoutError, whose message names the process that holds it. This does not check whether the process that held the lock is still running: releasing the lock of a writer that is still running lets other reads and writes run while its write is half done.

__sharedbox_box__

__sharedbox_box__(max_version: tuple[int, int] | None = None, **kwargs: Any) -> CapsuleType

Return a "sharedbox_box" capsule holding a handle with its own mapping of the segment.

Python code does not call it; an extension that accepts a box does. The capsule holds a pointer to an sbx_handle, declared in sharedbox/sharedbox_c.h. The handle's mapping is made from the box's OS handle, so closing or unlinking the box does not affect it. A consumer takes the handle with sharedbox::handle::from_capsule (C++) or sbx_import (C), renames the capsule "used_sharedbox_box", compares the box's schema hash with the one it expects, and destroys (C++) or releases (C) the handle itself. A capsule that is never taken releases the handle when it is garbage collected. Releasing touches no Python objects and does not need the GIL, so it may run on any thread and after interpreter shutdown.

Parameters:

Name Type Description Default
max_version tuple[int, int] | None

The (major, minor) layout version the caller supports; None means the box's own.

None

Raises:

Type Description
BufferError

If the major version of max_version differs from the box's layout, or if called from __post_init__, before the box is published.

NotImplementedError

If any other keyword is given.

Notes

On Windows the segment stays in existence while a handle is held.

close

close() -> None

Detach from the segment; other boxes on it keep working and the data stays.

Use unlink to remove it. Also stops this box's watcher thread, closes every box this handle attached to read a reference field, and stops all forwarding started through its events. Later reads and writes raise BoxClosedError. Calling it again does nothing. Leaving a with block calls it.

Called on a box's watcher thread, from an event callback, it does not wait for the watcher threads of the boxes it closes, and drops the writes those threads had not seen yet.

A box that is garbage collected is closed. Forwarding it started keeps running until its events group and every group follow returned are garbage collected too, which happens only when the cycle collector runs.

field

field(*, default: Any = MISSING, default_factory: Callable[[], Any] | Any = MISSING, init: bool = True, repr: bool = True, kw_only: bool | Any = MISSING, metadata: Mapping[Any, Any] | None = None, doc: str | None = None) -> Any

Set the options of one field of a SharedBox class, as dataclasses.field does.

A plain class attribute is still the field's default. There is no compare or hash option: two boxes are equal only if they are the same object. A field with neither default nor default_factory is required. A default is checked against the field's type and capacity when the class is defined, and a factory's result when a box is created; both raise what assigning the value raises. Values are copied into the segment, and only the stored types exist: no lists, dicts or other objects. Capacity goes in the annotation, not here.

A positional field without a default cannot follow one with a default; a field with default or default_factory counts as having one, and init=False fields take no position and are not counted.

Whether a field is keyword-only is fixed by the class that declares it, through its kw_only class keyword, a dataclasses.KW_ONLY among its own annotations, or kw_only here. A subclass keeps each inherited field's setting: its own kw_only=True affects only its own fields, and the fields of a kw_only base stay keyword-only in a subclass without the keyword.

A subclass that declares an inherited field again, with an annotation, gives it default options, as in dataclasses. An annotation without a value keeps only a plain inherited default: the other options go back to their defaults, and a field whose base gave it a default_factory becomes required. The same holds for an InitVar declared over an inherited field.

Parameters:

Name Type Description Default
default_factory Callable[[], Any] | Any

Called once per creation that is given no value for the field. The result is stored like any other value, so the box never keeps the object the factory returned.

MISSING
init bool

False leaves the field out of the constructor; the field then needs default or default_factory.

True
repr bool

False leaves the field out of repr() of a box.

True
kw_only bool | Any

Make this field keyword-only or not, whatever the kw_only class keyword and KW_ONLY say.

MISSING
metadata Mapping[Any, Any] | None

Kept in Python as a read-only mapping, never stored in the segment.

None
doc str | None

Kept in Python, never stored in the segment.

None

Raises:

Type Description
ValueError

If both default and default_factory are given.

TypeError

When the class is defined, for field() on a name that is not a field (a name starting with _, a ClassVar), field() without an annotation, init=False without a default, or a subclass that sets a plain class attribute, without an annotation, on the name of an inherited field.

fields

fields(class_or_box: type[SharedBox] | SharedBox) -> tuple[Field, ...]

Return one Field per field of a SharedBox subclass or box, in declaration order.

InitVar annotations are left out. This is how a field's metadata and doc are read.

Raises:

Type Description
TypeError

If the argument is neither a subclass of SharedBox nor one of its boxes.

Field dataclass

Field(name: str, type: Any, default: Any, default_factory: Any, init: bool, repr: bool, kw_only: Any, metadata: Mapping[Any, Any], doc: str | None)

Options of one field of a SharedBox class, as field takes them.

fields returns them. A Field is read-only, and two are equal only if they are the same object, as with dataclasses.Field.

type instance-attribute

type: Any

The evaluated annotation, with Annotated extras kept.

default instance-attribute

default: Any

dataclasses.MISSING when there is none.

default_factory instance-attribute

default_factory: Any

A function of no arguments, or dataclasses.MISSING.

init instance-attribute

init: bool

False if the constructor takes no value for the field.

repr instance-attribute

repr: bool

False if repr() of a box leaves the field out.

kw_only instance-attribute

kw_only: Any

True if the constructor takes the value by keyword only; dataclasses.MISSING until the class is created.

metadata instance-attribute

metadata: Mapping[Any, Any]

Read-only; kept in Python, never stored in the segment.

doc instance-attribute

doc: str | None

Kept in Python, never stored in the segment.

Capacity dataclass

Capacity(size: int)

Maximum size of a field: bytes for str, bytes, bytearray and Decimal, elements for a collection.

It goes in the annotation, as in Annotated[str, Capacity(32)] or Annotated[list[int], Capacity(16)], and allows 1 to 1048576 (1 Mi).

Raises:

Type Description
ValueError

If size is not between 1 and 1 Mi.

size instance-attribute

size: int

Bytes for text and bytes, not characters: a UTF-8 character can take up to 4 bytes; elements for a collection.