C-style packed binary structs for Python, declared with type annotations.
Zero runtime dependencies, byte-for-byte compatible with C struct
layouts (including __attribute__((packed))), and friendly to
cross-language interop and network protocols.
pip install structedfrom typing import Annotated
from structed import Endian, Struct, uint8_t, uint32_t, char, cstring, cppstring
class Header(Struct, endian=Endian.LITTLE):
magic : Annotated[bytes, char[4]] # fixed-width, NUL-padded
version: Annotated[int, uint8_t]
count : Annotated[int, uint32_t]
note : Annotated[str, cstring[16]] # NUL-terminated C string
data : Annotated[str, cppstring] # length-prefixed string
binary = Header(magic=b"PROT", version=1, count=3, note="hi", data="x").pack()
header = Header.unpack(binary)
obj, rest = Header.unpack_tail(binary + b"more") # stream framing| Marker | Wire type | Python type | Notes |
|---|---|---|---|
uint8_t .. uint64_t, int8_t .. int64_t |
fixed-width int | int |
byte order follows the struct's endian |
float32, float64 |
IEEE-754 | float |
|
char[N] |
char buf[N] |
bytes or str |
fixed width; pads with NULs |
char[N] + keep_nulls |
as above | str |
Annotated[str, char[N], keep_nulls]; keeps embedded NULs on unpack |
cstring[N] |
C string | str or bytes |
N includes the NUL terminator; unpack stops at the first NUL |
cppstring |
u32 length + payload |
str or bytes |
length-prefixed; variable-length |
Array(N, T), list[T], or scalar * N |
T arr[N] |
list |
fixed-size array of primitives or structs |
| nested struct class | struct T {...} |
instance | a Struct subclass used bare as an annotation |
Annotations use typing.Annotated: Annotated[int, uint16_t],
Annotated[str, cstring[16]], Annotated[str, cppstring].
Nested structs are declared as a bare annotation (inner: Point); each keeps
its own endian regardless of the parent struct's byte order.
An array packs N elements back-to-back and round-trips as a list. The
element type is given explicitly, inferred from a list[T] annotation, or
repeated with *:
class S(Struct, endian=Endian.LITTLE):
flags: Annotated[list[int], Array(4, uint16_t)] # explicit
ages : Annotated[list[int], uint8_t * 2] # shorthand for Array(2, uint8_t)
pts : Annotated[list[Point], Array(3)] # array of nested structs
s = S(flags=[1, 2, 3, 4], ages=[12, 13], pts=[...])
assert s.flags == [1, 2, 3, 4] and s.ages == [12, 13]The value must have exactly N elements, otherwise packing raises
PackingError. Arrays cannot hold variable-length (cppstring) elements.
class Packet(Struct, endian=Endian.BIG, packed=True, truncate=False):
...endian:Endian.LITTLE,Endian.BIG,Endian.NATIVE,Endian.NETWORK.packed:True(default) lays fields back-to-back like C__attribute__((packed));Falseapplies natural C alignment.truncate: whenFalse(default) a value longer than itschar[N]/cstring[N]field raisesPackingError; whenTrueit is silently cut to fit (and still NUL-terminated forcstring).
The same options are available via the binary_struct(endian=..., packed=..., truncate=...) class decorator.
obj.pack()->bytesS.unpack(data)->S(consumes from the front)S.unpack_tail(data)->(S, tail)for stream framingS.unpack_from(data, offset)->SS.sizeof()->int(fixed minimum for variable-length structs)
cppstring fields are length-prefixed and may be followed by fixed-size
fields; parsing always resumes right after the payload. Only one variable
field is allowed per struct, packed=True is required, and a struct with a
variable field cannot be nested or used as an array element.
make venv # create .venv and install editable + deps
make check # run tests + syntax + type checks
make example # run examples/
make build-release # verify, then build sdist + wheel