Files and multiple messages¶
After encode(), you have ordinary bytes. You can save them or concatenate them with other messages. These examples use a small model:
from bytespec import ProtoModel
class Message(ProtoModel):
text: str
Save and read a file¶
from pathlib import Path
message = Message(text="Hello")
path = Path("message.bin")
path.write_bytes(message.encode())
restored = Message.decode(path.read_bytes())
print(restored.text) # Hello
Use the binary operations write_bytes() and read_bytes(): no additional text encoding is needed.
Modify and encode again¶
restored.text = "Updated"
print(Message.decode(restored.encode()).text) # Updated
Model attributes are mutable. Each encode() call reads their current values, including determining again which optional fields are present. Bytes returned by an earlier call do not change.
Read two messages from one buffer¶
decode() expects exactly one message. If the buffer contains several, use decode_from():
buffer = Message(text="One").encode() + Message(text="Two").encode()
first, offset = Message.decode_from(buffer, 0)
second, offset = Message.decode_from(buffer, offset)
print(first.text, second.text) # One Two
print(buffer[offset:]) # b''
The second result is the absolute position after the decoded message. Pass it to the next call. The starting position must be non-negative.
Start decoding after a transport prefix¶
buffer = b"MSG:" + Message(text="Hello").encode() + b"NEXT"
message, end = Message.decode_from(buffer, 4)
print(message.text) # Hello
print(buffer[end:]) # b'NEXT'
end refers to the entire buffer, not to a slice after MSG:. The remaining bytes are left to the caller.
Both methods read an existing buffer. They do not accumulate network fragments between calls: an incomplete message raises DecodeError. Assemble it first using your transport. Size checks and rules for extra data are described in Boundaries and unknown data.
If the buffer contains messages of different types, the application must choose the correct class for each call. There is no automatic model selection by identifier; see Binary / wire format for header details.
Encoding without Constructor¶
If an outer layer already writes the identifier, you can skip this element while keeping the other header parts:
from bytespec import Constructor, Flags, PayloadLength
from bytespec.types import UInt8
class Reading(ProtoModel):
__header__ = (Flags(1), Constructor(1), PayloadLength(1))
value: UInt8
reading = Reading(value=7)
encoded = reading.encode(include_constructor=False)
restored, end = Reading.decode_from(b"xx" + encoded, 2, expect_constructor=False)
assert restored == reading
print(encoded.hex(" "), end)
00 01 07 5
Both parameters mean that the Constructor bytes are entirely absent, wherever the element appears in the header. expect_constructor=False does not read and ignore an arbitrary identifier in the input. decode() has no such parameter; use decode_from() and check the final offset if needed. ModelCodec applies this pair of parameters automatically for nested models.
Next: Binary / wire format covers exact bytes, boundaries, and unknown data.