Числа и другие типы

У пользователя уже есть имя. Добавим идентификатор:

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 означает целое без знака размером 32 бита — 4 байта. Значение 42 остаётся обычным Python int; аннотация выбирает способ записи. Обычный int использует знаковый VarInt (ZigZag), а фиксированный размер выбирается явно. Для float нужно указать Float32 или Float64.

Как выбрать целое число

Аннотация

Размер

Значения

UInt8, UInt16, UInt32, UInt64

1, 2, 4, 8 байт

От 0 до 2**N - 1, где N — число бит

Int8, Int16, Int32, Int64

1, 2, 4, 8 байт

От -2**(N-1) до 2**(N-1) - 1

VarUInt

1–10 байт

Диапазон UInt64; малые значения занимают меньше места

VarInt

1–10 байт

Диапазон Int64; поддерживает отрицательные значения

Например, UInt8 подходит для чисел от 0 до 255. Значение вне диапазона будет отклонено при encode(), а не при создании экземпляра. Все эти аннотации импортируются из bytespec.types.

Малые числа переменной длины

int и VarInt используют одинаковый формат; VarUInt подходит для неотрицательных значений. Посмотрим на байты полей:

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 — ZigZag-представление -2, ac 02 — unsigned varint 300. Срез [14:] пропускает стандартный header, как в первой модели. Все varints ограничены 64-битными диапазонами, даже для обычного Python int.

Числа с дробной частью

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 занимает 4 байта, Float64 — 8. При записи в Float32 Python float округляется до 32-битного представления, поэтому для сравнения произвольных дробных значений используйте допуск.

Строки и байты

str и bool уже знакомы из первой модели. Для бинарных данных есть bytes:

class Packet(ProtoModel):
    data: bytes

packet = Packet.decode(Packet(data=b"ABC").encode())
print(packet.data)  # b'ABC'

По умолчанию строки используют UTF-8. Длина строк и bytes записывается автоматически; настраивать её для обычного использования не нужно. Позже изменим эти параметры в Строки, длины и настройки полей.

UUID и дата

Типы стандартной библиотеки можно использовать прямо в аннотациях:

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

UUID занимает ровно 16 байт. Дата записывается строкой ISO, сохраняя представленное в ней смещение UTC. Имя часовой зоны не передаётся; дата без tzinfo после чтения тоже остаётся без него. Отдельные datetime.date и datetime.time не поддерживаются автоматически: используйте свой codec, если формат требует именно их. DatetimeCodec восстанавливает datetime, а не эти типы.

Набор именованных значений

Для статуса сообщения подходит строковый enum:

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

Записывается строковое значение "ready", а при чтении восстанавливается Status.READY. Неизвестное значение вызывает DecodeError. Числовой IntEnum тоже поддерживается автоматически:

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

По умолчанию числовое .value кодируется через VarInt, поэтому значение 2 занимает байт 04, а не 02. При чтении восстанавливается член enum. Для фиксированного unsigned byte используйте EnumCodec(Command, UInt8Codec())Составить codec из готовых частей. Обычный Enum без наследования от str или int требует явного codec.

Мы разобрали отдельные значения. Далее — Необязательные поля: как выразить, что значения может не быть. Полная таблица поддерживаемых аннотаций доступна в Поддерживаемые аннотации.