Lists and nested models¶
Multiple values of one type¶
A user can belong to several groups:
from bytespec import ProtoModel, field
class User(ProtoModel):
name: str
groups: list[str]
user = User(name="Anna", groups=["readers", "editors"])
decoded = User.decode(user.encode())
print(decoded.groups) # ['readers', 'editors']
list[str] means a list of strings. list[UInt32] and lists of other supported types work the same way. A list can be empty.
A separate empty list for each user¶
To avoid passing groups=[] manually, use default_factory:
class User(ProtoModel):
name: str
groups: list[str] = field(default_factory=list)
anna = User(name="Anna")
boris = User(name="Boris")
anna.groups.append("editors")
print(anna.groups) # ['editors']
print(boris.groups) # []
The list factory is called without arguments when each instance is created, if the field was omitted. This gives each user a separate list. default=[] would instead use one shared object.
The factory can also be another function that takes no arguments, such as uuid4 for a UUID field. It is not called when the class is declared. Set either default or default_factory, never both. The rules for validating their values are covered in Defaults and instance creation.
A model as a field of another model¶
Let us use the User already defined above as the message author:
class Message(ProtoModel):
author: User
text: str
message = Message(author=anna, text="Hello!")
decoded = Message.decode(message.encode())
print(decoded.author.name) # Anna
print(decoded.author.groups) # ['editors']
print(decoded.text) # Hello!
Pass a User instance, not a dictionary. When reading a Message, the library restores the nested User automatically. Declare a nested class before the class that uses it: annotations are resolved immediately.
A list of models¶
For multiple users, list[User] is enough:
class Packet(ProtoModel):
users: list[User]
packet = Packet(users=[anna, boris])
decoded = Packet.decode(packet.encode())
print([user.name for user in decoded.users]) # ['Anna', 'Boris']
Nested lists, such as list[list[str]], are also supported. The list itself can be optional: groups: list[str] | None = field(flag=0). In that case, None and [] are distinct values. Optional elements such as list[str | None] are not supported, nor is automatic serialization of dict, tuple, or set.
A nested model keeps its own framing, omitting only the constructor because its type is already known from the annotation. The outer class settings do not replace the nested model settings. The exact bytes are explained later in How nested model headers are written.
Ordinary lists and nested models need no format configuration. To change the text encoding or length prefix, continue to Strings, lengths, and field settings. The full layout of nested data is described separately in Binary / wire format.