Optional fields

An email address may be unknown. Express this with str | None:

from bytespec import ProtoModel, field

class User(ProtoModel):
    name: str
    email: str | None = field(flag=0)

user = User(name="Anna")
decoded = User.decode(user.encode())
print(decoded.email)  # None

str | None allows a string or an absent value. You can now omit the email argument; without a default, it becomes None. Optional[str] from typing means the same thing.

flag=0 assigns the field a presence bit in the message. The library sets it when a value is present and clears it for None. You do not need to calculate or pass flags manually when reading.

Supply a value

user = User(name="Anna", email="anna@example.com")
decoded = User.decode(user.encode())
print(decoded.email)  # anna@example.com

When the value is None, no field bytes are written. An empty string, however, is still a present value:

user = User(name="Anna", email="")
decoded = User.decode(user.encode())
print(repr(decoded.email))  # ''

The same distinction applies between None and numeric zero or False. Only None means absence.

Add another optional field

Each field needs its own bit number:

class User(ProtoModel):
    name: str
    email: str | None = field(flag=0)
    nickname: str | None = field(flag=1)

user = User(name="Anna", nickname="ann")
decoded = User.decode(user.encode())
print(decoded.email, decoded.nickname)  # None ann

flag is a bit number, not a bitmask. By default, numbers 0 through 63 are available; duplicates are not allowed. An optional field without flag, or a regular field with flag, raises SchemaError when the class is declared. The bitmap size is set by the Flags element in Headers and framing. If you change the header, keep Flags for optional fields: omitting it raises SchemaError at class declaration. This check also includes inherited fields.

Optional fields with defaults

default still applies only when the argument is omitted:

class Message(ProtoModel):
    note: str | None = field(flag=0, default="draft")

print(Message().note)           # draft
print(Message(note=None).note)  # None

encoded = Message(note=None).encode()
print(Message.decode(encoded).note)  # None

An explicit None is not replaced with "draft". When decoding, an absent field also becomes None, regardless of its default. Defaults help create instances; they do not restore missing message bytes.

Next: Lists and nested models, where we will add a list to the user and nest it inside another model.