Skip to main content

FastProto is fast and efficient protobuf library for Python, built on the top of Rust.

Project description

FastProto

FastProto is a fast, ergonomic Protocol Buffers library for Python. Messages are plain, readable @dataclass types — no generated getters/setters, no Message reflection API to learn — while all encoding and decoding happens in a compiled Rust core.

  • Idiomatic messages. Generated code is a @dataclass, annotated with plain Python types (str, int, list[...], dict[...], | None). Autocomplete, type checkers, and repr() all just work.
  • Rust-powered wire codec. Encoding and decoding are implemented in Rust via PyO3, avoiding the overhead of pure-Python protobuf implementations.
  • Wire-compatible. Bytes produced by FastProto are read correctly by Google's reference protobuf runtime, and vice versa.
  • Drop-in protoc plugin. Reuses the standard .proto toolchain — run protoc with --fastproto_out to generate typed dataclasses.

Installation

pip install fastproto

Generating code from .proto files also requires the protoc compiler and the plugin's protobuf dependency:

pip install "fastproto[plugin]"

protoc itself isn't installed by pip — grab it from your package manager (apt install protobuf-compiler, brew install protobuf, ...) or the official releases.

Quick start

1. Write a .proto file (user.proto):

syntax = "proto3";
package example;

enum Role {
  ROLE_UNSPECIFIED = 0;
  ROLE_ADMIN = 1;
  ROLE_USER = 2;
}

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

message User {
  int64 id = 1;
  string name = 2;
  optional string email = 3;
  Role role = 4;
  repeated string tags = 5;
  Address address = 6;
  map<string, int32> counters = 7;
}

2. Generate the Python module with protoc, using the fastproto plugin:

protoc --proto_path=. --fastproto_out=. user.proto

This produces user_pb.py — a plain, readable dataclass module:

# @generated by fastproto. DO NOT EDIT.
# source: user.proto
from dataclasses import dataclass, field
from enum import IntEnum

from fastproto import Message, Scalar, message


class Role(IntEnum):
    ROLE_UNSPECIFIED = 0
    ROLE_ADMIN = 1
    ROLE_USER = 2


@message(_ADDRESS_DESCRIPTOR)
@dataclass(slots=True)
class Address(Message):
    city: Scalar.String = ""
    street: Scalar.String = ""


@message(_USER_DESCRIPTOR)
@dataclass(slots=True)
class User(Message):
    id: Scalar.Int64 = 0
    name: Scalar.String = ""
    email: Scalar.String | None = None
    role: Role = Role(0)
    tags: list[Scalar.String] = field(default_factory=list)
    address: "Address | None" = None
    counters: dict[Scalar.String, Scalar.Int32] = field(default_factory=dict)

3. Use it like any other Python dataclass:

from user_pb import Address, Role, User

user = User(
    id=42,
    name="Ada",
    email="ada@example.com",
    role=Role.ROLE_ADMIN,
    tags=["vip", "beta"],
    address=Address(city="London", street="Baker St"),
    counters={"logins": 7},
)

# Serialize to protobuf wire bytes.
data = user.to_bytes()

# Deserialize back into a `User` instance.
same_user = User.from_bytes(data)
assert same_user == user

That's it — no SerializeToString() / ParseFromString() ceremony, no ListFields() reflection, just to_bytes() / from_bytes() on a dataclass you can construct, compare, and pretty-print directly.

Field type mapping

Every proto scalar type has a corresponding alias under fastproto.Scalar. Each alias is just the underlying Python type (int, str, ...) tagged with an Annotated[...] marker, so it type-checks exactly as you'd expect while still documenting the precise wire type:

proto type Python annotation
double Scalar.Double
float Scalar.Float
int32 Scalar.Int32
int64 Scalar.Int64
uint32 Scalar.UInt32
uint64 Scalar.UInt64
sint32 Scalar.SInt32
sint64 Scalar.SInt64
fixed32 Scalar.Fixed32
fixed64 Scalar.Fixed64
sfixed32 Scalar.SFixed32
sfixed64 Scalar.SFixed64
bool Scalar.Bool
string Scalar.String
bytes Scalar.Bytes

Composite fields map the way you'd hope:

  • repeated Tlist[T]
  • map<K, V>dict[K, V]
  • optional T / oneof members → T | None
  • nested/enum messages → the generated class or IntEnum, referenced by name

Enums, nested messages, and oneof

Enums become IntEnum subclasses; message fields hold real instances of the generated dataclass (or None when unset); oneof groups are represented as plain optional fields, and FastProto enforces "at most one set" at encode time:

from user_pb import User

# Setting more than one oneof member raises when you try to serialize it.
User(phone="123", telegram="abc").to_bytes()  # raises ValueError: ... oneof ...

Presence semantics

FastProto follows proto3 field presence rules:

  • Plain scalar fields (string, int32, ...) use their zero value as the default and are not nullable — they always round-trip to a concrete value.
  • optional scalar fields, message fields, and oneof members are nullable (T | None) and track explicit presence, matching proto3 semantics exactly (an explicitly-set empty string is distinguishable from an unset field).
from user_pb import User

empty = User()
assert empty.to_bytes() == b""      # all-default messages encode to zero bytes
assert User.from_bytes(b"") == empty
assert empty.email is None           # optional, unset

How linking works

Generated dataclasses reference sibling messages and enums by name (as forward references), since a message can reference a type defined later in the same file, or itself (recursively). FastProto resolves these references lazily, on first to_bytes() / from_bytes() call, by looking them up in the generated module's namespace — you never need to call anything yourself.

Development

The project is a mixed Rust/Python codebase built with maturin, managed with uv.

git clone https://github.com/pkozhem/fastproto
cd fastproto
uv sync                        # installs dev dependencies and builds the extension
uv run maturin develop         # rebuild the Rust extension in-place after changes
uv run pytest                  # run the test suite
uv run ruff check .            # lint
uv run ty check                # type-check

Test fixtures under tests/generated/ are themselves @generated output, committed so the plugin's golden tests can diff against them. After editing a .proto file under tests/protos/ or the plugin itself, regenerate them with:

uv run python scripts/regen.py

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastproto-0.1.1.tar.gz (39.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastproto-0.1.1-cp314-cp314-macosx_11_0_arm64.whl (240.1 kB view details)

Uploaded CPython 3.14macOS 11.0+ ARM64

File details

Details for the file fastproto-0.1.1.tar.gz.

File metadata

  • Download URL: fastproto-0.1.1.tar.gz
  • Upload date:
  • Size: 39.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.14.1

File hashes

Hashes for fastproto-0.1.1.tar.gz
Algorithm Hash digest
SHA256 cd3c22a9beefe3002f50ab0523c27fbf0add59524d2983af9fcac0a2f8d0490a
MD5 a9d572415e04943cd495fe644d9268b9
BLAKE2b-256 7ff9a41bb38c72f2497b349bc898b6a1b1a1d814d9090ea34475c9131ee5ffd0

See more details on using hashes here.

File details

Details for the file fastproto-0.1.1-cp314-cp314-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for fastproto-0.1.1-cp314-cp314-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 db9f8f2c166ec027a2cf33168deec2869224034fa613a155e49040b5a0a4c3d3
MD5 43fc693159a21565f6245d37f0a8fe72
BLAKE2b-256 226af5fd7f8abf2bcac8833ade22492aa9b1b91871e9d7c5bb104368da9eebe5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page