3.2.2.2. Struct#

A struct describes a finite collection of named fields. In contrast to a sequence, a struct utilizes Python classes as its model. The annotation feature in Python enables the definition of custom types as annotations, enabling this special struct class to create a model solely based on class annotations. Additionally, it generates a dataclass of the provided model, offering a standardized string representation.

Several differences exist between a Sequence and a Struct, with the most significant ones highlighted below:

Behaviour of structs and sequences#

Sequence

Struct

Model Type

dict

type

Inheritance

No

Yes

Attribute Access

x["name"]

getattr(x, "name", None)

Unpacked Type (also needed to pack)

dict [*]

instance of model

Documentation

No

Yes

As evident from the comparison, the Struct class introduces new features such as inheritance and documentation support. It’s crucial to note that inheritance uses struct types exclusively.

The Sequence class implements a specific process for creating an internal representation of the given model. The Struct class enhances this process by handling default values, replacing types for documentation purposes, or removing annotation fields directly from the model. Additionally, this class adds __struct__ to the model afterward.

Implementation Note

If you decide to use the annotation feature from the __future__ module, it is necessary to enable S_EVAL_ANNOTATIONS since it “Stringizes” all annotations. inspect then evaluates all strings, introducing a potential security risk. Exercise with caution when evaluating code!

Specifying structs is as simple as defining Python Classes:

>>> @struct
... class BaseFormat:
...     magic: b"MAGIC"
...     a: uint8
...

Internally, a representation with all required fields and their corresponding names is created. As b"MAGIC" or uint8 are instances of types, the type replacement for documentation purposes should be enabled, as shown in Special Methods for Struct-Like objects.

As described above, this class introduces an easy-to-use inheritance system using the method resolution order of Python:

>>> @struct
... class Format(BaseFormat):
...     b: uint32
...     c: uint16
...
>>> list(Format.__struct__.get_members())
['magic', 'a', 'b', 'c']

Inherited fields are copied into the derived structure as new field wrappers. Mutating derived field metadata, for example changing byte order on Format.__struct__, will not mutate the base structure. If several base classes provide the same field name, Python MRO precedence decides which field is imported. Fields declared directly on the child class override inherited fields in-place, keeping the binary layout position stable.

Byte order and architecture configured on the child structure apply to fields declared by the child. Inherited field wrappers keep their own explicit or base-derived configuration unless the child overrides the field declaration.

If a base field has a default value and the child adds required fields, the child fields are made keyword-only so the binary field order can remain unchanged while still satisfying dataclass constructor rules.

Inherited fields can be removed from the derived binary layout explicitly with RemoveField:

>>> @struct
... class Child(BaseFormat):
...     magic: RemoveField()
...     b: uint32
...

This removes magic from Child only; BaseFormat remains unchanged.

Programmers Note

As the Struct class is a direct subclass of Sequence, nesting is supported by default. That means, so-called anonymous inner structs can be defined within a class definition.

>>> @struct
... class Format:
...     a: uint32
...     b: {"c": uint8}
...

It is not recommended to use this technique as the inner structs can’t be used anywhere else. Anonymous inner union definitions are tricky and are not officially supported yet. There are workarounds to that problem, which are discussed in the API documentation of Sequence.