Numbers and other types¶
Our user already has a name. Let us add an identifier:
from bytespec import ProtoModel, field
from bytespec.types import UInt32
class User(ProtoModel):
id: UInt32
name: str
active: bool = field(default=True)
user = User(id=42, name="Anna")
decoded = User.decode(user.encode())
print(decoded.id) # 42
UInt32 means an unsigned 32-bit integer: 4 bytes. The value 42 remains an ordinary Python int; the annotation selects its representation. A plain int uses signed VarInt (ZigZag), while a fixed size must be chosen explicitly. For float, you must specify Float32 or Float64.
Choosing an integer type¶
Annotation |
Size |
Values |
|---|---|---|
|
1, 2, 4, or 8 bytes |
From |
|
1, 2, 4, or 8 bytes |
From |
|
1–10 bytes |
The |
|
1–10 bytes |
The |
For example, UInt8 accepts numbers from 0 to 255. An out-of-range value is rejected by encode(), not when the instance is created. All these annotations are imported from bytespec.types.
Small variable-length integers¶
int and VarInt use the same format; VarUInt is suitable for nonnegative values. Let us inspect the field bytes:
from bytespec.types import VarUInt
class Counters(ProtoModel):
delta: int
total: VarUInt
counters = Counters(delta=-2, total=300)
assert Counters.decode(counters.encode()) == counters
print(counters.encode()[14:].hex(" "))
03 ac 02
03 is the ZigZag representation of -2, and ac 02 is the unsigned varint representation of 300. The [14:] slice skips the standard header, as in the first model. All varints are limited to 64-bit ranges, even for a plain Python int.
Floating-point numbers¶
from bytespec.types import Float32
class Point(ProtoModel):
x: Float32
y: Float32
point = Point.decode(Point(x=1.25, y=3.5).encode())
print(point.x, point.y) # 1.25 3.5
Float32 takes 4 bytes, and Float64 takes 8. Encoding a Python float as Float32 rounds it to a 32-bit representation, so use a tolerance when comparing arbitrary floating-point values.
Strings and bytes¶
We have already used str and bool in the first model. For binary data, use bytes:
class Packet(ProtoModel):
data: bytes
packet = Packet.decode(Packet(data=b"ABC").encode())
print(packet.data) # b'ABC'
Strings use UTF-8 by default. String and bytes lengths are written automatically; ordinary usage needs no configuration. We will change these settings later in Strings, lengths, and field settings.
UUID and datetime¶
Standard library types can be used directly in annotations:
from datetime import datetime, timezone
from uuid import UUID
class Message(ProtoModel):
id: UUID
created_at: datetime
message = Message(
id=UUID("12345678-1234-5678-1234-567812345678"),
created_at=datetime(2026, 9, 7, 12, 0, tzinfo=timezone.utc),
)
decoded = Message.decode(message.encode())
print(decoded.id) # 12345678-1234-5678-1234-567812345678
print(decoded.created_at.isoformat()) # 2026-09-07T12:00:00+00:00
A UUID takes exactly 16 bytes. A datetime is written as an ISO string, preserving its UTC offset. The time zone name is not transmitted; a datetime without tzinfo remains without it after decoding. Separate datetime.date and datetime.time values are not supported automatically: use a custom codec if your format requires them. DatetimeCodec restores a datetime, not those types.
Named values¶
A string enum is a good fit for a message status:
from enum import Enum
class Status(str, Enum):
READY = "ready"
BUSY = "busy"
class Message(ProtoModel):
status: Status
message = Message(status=Status.READY)
decoded = Message.decode(message.encode())
print(decoded.status.value) # ready
The string value "ready" is written, and decoding restores Status.READY. An unknown value raises DecodeError. Numeric IntEnum types are also supported automatically:
from enum import IntEnum
class Command(IntEnum):
READ = 1
WRITE = 2
class Request(ProtoModel):
command: Command
request = Request(command=Command.WRITE)
restored = Request.decode(request.encode())
assert restored.command is Command.WRITE
print(request.encode()[14:].hex(" "))
04
By default, the numeric .value is encoded with VarInt, so the value 2 takes the byte 04, not 02. Decoding restores the enum member. For a fixed unsigned byte, use EnumCodec(Command, UInt8Codec()); see Compose a codec from existing parts. A plain Enum that does not inherit from str or int requires an explicit codec.
We have covered individual values. Next: Optional fields, which explains how to represent a value that may be absent. The full table of supported annotations is available in Supported annotations.