Your first model

Install bytespec in a Python 3.10+ environment:

python -m pip install bytespec

You can save this example as user.py and run it as a complete script:

from bytespec import ProtoModel

class User(ProtoModel):
    name: str
    active: bool

user = User(name="Anna", active=True)
encoded = user.encode()
decoded = User.decode(encoded)

print(decoded.name)    # Anna
print(decoded.active)  # True

Now let us look at the three steps in this example.

Define and create a model

User inherits from ProtoModel. The annotations name: str and active: bool describe two message fields. Strings and booleans need no additional configuration.

Values are passed by name and accessed as attributes:

user = User(name="Boris", active=False)
print(user.name)  # Boris

Both fields are required for now: you must supply them when creating a User. On the next page, we will add a default value.

Encode to bytes

encoded = user.encode()
print(type(encoded).__name__)  # bytes

encode() returns a complete binary message. You can save it to a file or send it over the network without converting it to text. After the standard header, name and active are written in declaration order. The library calculates the lengths of the text and the entire body; you do not need to advance offsets manually or maintain a separate encoding function.

Decode it back

decoded = User.decode(encoded)
print(decoded.name)    # Boris
print(decoded.active)  # False

decode() is called on the class and creates a new User. Pass it the bytes of one message. The recipient must know the model: the library does not automatically select a class from the contents.

That is the usual workflow: class → instance → bytes → instance. When changing fields or their settings, keep the sender and recipient models in sync.

Inspect the bytes

For the first model, User(name="Anna", active=True), the field contents are:

encoded = User(name="Anna", active=True).encode()
print(encoded[14:].hex(" "))
00 00 00 04 41 6e 6e 61 01

Four bytes for the string length, UTF-8 Anna, then 01 for True. The slice skips the 14-byte standard header. For a different protocol, you can choose the integer size, string prefix, and framing itself; these are introduced in Numbers and other types, Strings, lengths, and field settings, and Headers and framing.

Next: Fields and default values, where we will give active a default value.