Skip to main content

proto

Protocol Buffers without the toolchain. proto lets you declare binary messages as ordinary Python dataclasses and serialize them to bytes that are wire-compatible with Google's Protocol Buffers (proto3). No protoc, no generated code, no C extension, no dependencies: just the standard library.

Use it when you need to talk to a protobuf service from a script, persist compact records, or prototype a schema in Python first, and still have every other protobuf implementation read your bytes.

import proto

@proto.message
class Point:
    x: int = proto.field(1, "sint32")
    y: int = proto.field(2, "sint32")

data = proto.encode(Point(3, -4))     # b'\x08\x06\x10\x07'
assert proto.decode(Point, data) == Point(3, -4)

Features

  • Wire-compatible: varints, zigzag, fixed-width, length-delimited and packed encodings match the official protobuf encoding byte for byte. The tests check the exact byte strings from the protobuf encoding guide.
  • Schema-first dataclasses: field numbers live next to the type annotations. Messages are real dataclasses, so you keep ==, repr, replace() and your type checker.
  • All 15 scalar types: int32 int64 uint32 uint64 sint32 sint64 bool fixed32 fixed64 sfixed32 sfixed64 float double string bytes, plus IntEnum enums, nested and recursive messages, repeated fields, and proto3 optional.
  • proto3 semantics: default values are omitted on the wire, unknown fields are skipped, packed and unpacked repeated fields are both accepted, unknown enum values are kept as int (open enums).
  • Strict validation: out-of-range integers, wrong Python types, malformed or truncated input raise clear EncodeError / DecodeError / SchemaError.
  • .proto export: to_proto() renders your classes as a .proto file, so other languages can generate code from the same schema.
  • Streaming: length-delimited framing (writeDelimitedTo format) for many messages in one file or socket.
  • Pure Python 3.10+, fully type-hinted (py.typed), zero dependencies.

Install

pip install proto            # once published
pip install -e .             # from a checkout

Note: Google's proto-plus package also installs a top-level proto module. Don't install both in the same environment.

Quickstart

from __future__ import annotations

import enum
import io

import proto


class Role(enum.IntEnum):
    ROLE_UNSPECIFIED = 0     # proto3 enums need a zero value
    ADMIN = 1
    MEMBER = 2


@proto.message
class Address:
    city: str = proto.field(1)
    zip_code: str = proto.field(2)


@proto.message
class User:
    name: str = proto.field(1)                      # inferred: string
    id: int = proto.field(2, "uint32")              # explicit scalar type
    age: int | None = proto.field(3, "int32")       # proto3 `optional`: None = unset
    role: Role = proto.field(4)                     # enum
    emails: list[str] = proto.field(5)              # repeated
    scores: list[int] = proto.field(6, "sint32")    # repeated, packed by default
    address: Address | None = proto.field(7)        # nested message
    friends: list[User] = proto.field(8)            # recursive


ada = User("ada", id=1, role=Role.ADMIN, emails=["ada@example.com"],
           address=Address("London", "N1"))

data = ada.to_bytes()                 # same as proto.encode(ada)
again = User.from_bytes(data)         # same as proto.decode(User, data)
assert again == ada

proto.to_dict(ada)
# {'name': 'ada', 'id': 1, 'role': 'ADMIN', 'emails': ['ada@example.com'],
#  'scores': [], 'address': {'city': 'London', 'zip_code': 'N1'}, 'friends': []}

# Many messages in one stream
buf = io.BytesIO()
for user in (ada, User("bob", id=2)):
    proto.write_delimited(buf, user)
buf.seek(0)
names = [u.name for u in proto.iter_delimited(User, buf)]   # ['ada', 'bob']

print(proto.to_proto(User, package="example.v1"))

The last line prints:

syntax = "proto3";

package example.v1;

enum Role {
  ROLE_UNSPECIFIED = 0;
  ADMIN = 1;
  MEMBER = 2;
}

message Address {
  string city = 1;
  string zip_code = 2;
}

message User {
  string name = 1;
  uint32 id = 2;
  optional int32 age = 3;
  Role role = 4;
  repeated string emails = 5;
  repeated sint32 scores = 6;
  Address address = 7;
  repeated User friends = 8;
}

Declaring fields

Annotation Inferred .proto type Default when omitted
int int64 0
float double 0.0
bool bool False
str string ""
bytes bytes b""
SomeIntEnum SomeIntEnum the member with value 0
SomeMessage | None SomeMessage None (unset)
T | None (scalar) optional T None (unset)
list[T] repeated T []

Pass type= (the second argument of field) to choose another scalar encoding such as "sint32" or "fixed64", or to name an enum/message class explicitly.

API overview

Everything is importable from the top-level proto package.

Name Description
@message / @message(name="Wire") Class decorator. Turns an annotated class into a dataclass-based message and adds to_bytes() and classmethod from_bytes(data). name sets the name used by to_proto (default: class name).
field(number, type=None, *, default=..., default_factory=..., packed=None) Declares a field. type is a scalar name, an IntEnum subclass or a message class; it is inferred from the annotation when omitted. packed=False disables packed encoding for repeated numeric/enum fields. Every annotated attribute must use field().
encode(msg) -> bytes Serialize a message instance.
decode(cls, data) -> cls Parse bytes/bytearray/memoryview into a new cls instance. Absent fields get their proto3 zero value; the last occurrence of a singular field wins.
fields(cls) -> tuple[FieldInfo, ...] Resolved schema of a message class, ordered by field number.
FieldInfo Frozen dataclass: name, number, kind ("scalar"/"enum"/"message"), type_name, repeated, optional, packed, scalar, target, and property wire_type.
is_message(obj) -> bool True for @message classes and their instances.
to_dict(msg) -> dict Plain-dict view: nested messages become dicts, enums become names, unset optionals are omitted.
from_dict(cls, data) -> cls Inverse of to_dict; enums may be given by name or number.
to_proto(*classes, package=None) -> str Render the classes and every enum/message they reference as proto3 source.
write_delimited(stream, msg) -> int Write a varint length prefix plus the message; returns bytes written.
read_delimited(cls, stream) -> cls | None Read one framed message; None at a clean end of stream.
iter_delimited(cls, stream) Iterate framed messages until the stream is exhausted.
ProtoError Base exception. Subclasses: SchemaError (also a TypeError), EncodeError and DecodeError (also ValueError).
__version__ "0.1.0"

Low-level primitives live in proto.wire:

Name Description
WireType IntEnum: VARINT, I64, LEN, SGROUP, EGROUP, I32.
MAX_FIELD_NUMBER 2**29 - 1.
encode_varint(value) -> bytes Base-128 varint; negatives use 64-bit two's complement.
decode_varint(data, pos=0) -> (value, new_pos) Decode one varint.
zigzag_encode(value, bits=64) -> int / zigzag_decode(value) -> int ZigZag mapping used by sint32/sint64.
encode_tag(number, wire_type) -> bytes Field key.
decode_tag(data, pos=0) -> (number, wire_type, new_pos) Parse a field key.
skip_field(data, pos, wire_type, number=None) -> int Skip an unknown field's payload, groups included.

Limitations

proto 0.1 covers the core of proto3. Not supported yet: oneof, map<K, V> fields, well-known types (Timestamp, Any, ...), proto2 groups (they are skipped when decoding), services/gRPC, and merging of repeated occurrences of a singular message field (the last occurrence wins). Annotations that name other classes must be resolvable from the scope where the class is defined.

Development

PYTHONPATH=src python3 -m unittest discover -s tests -v   # standard library only
python3 -m pytest                                        # if pytest is installed

License

MIT

Metadata

Release files for proto 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for proto 0.1.0
File Size Uploaded
proto-0.1.0.tar.gz 23.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for proto 0.1.0
File Interpreter ABI Platform
proto-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 42.1 kB

Release files / proto-0.1.0.tar.gz

Download URL proto-0.1.0.tar.gz
Size 23.6 kB
Tags Source
SHA-256 checksum
How to use checksums
60382135e8c89241525b9d5a7f7156fb214dbbd50ccbb9199c387998d603499b
BLAKE2b-256 checksum
How to use checksums
d06173e6affa2fb7fb8562bac36ff5e7f3c120c7a7cd9f9da161d1ef711d3eba
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / proto-0.1.0-py3-none-any.whl

Download URL proto-0.1.0-py3-none-any.whl
Size 18.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6104bab93ac6c12eb73a747ce34ea4474ae1cc54cb9ed9cd84ff40c86bbabadb
BLAKE2b-256 checksum
How to use checksums
65b6f76904cb906ac1eb609a2dab6deaf67563c82739ae472b3e66a022accd1a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

0.0.1

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page