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, plusIntEnumenums, nested and recursive messages,repeatedfields, and proto3optional. - 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. .protoexport:to_proto()renders your classes as a.protofile, so other languages can generate code from the same schema.- Streaming: length-delimited framing (
writeDelimitedToformat) 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-pluspackage also installs a top-levelprotomodule. 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)
| File | Size | Uploaded | |
|---|---|---|---|
| proto-0.1.0.tar.gz | 23.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|