Profile
Back to NewsBack
GitHub Trending 4 min
Reader Mode
bufbuild/protobuf-py: Idiomatic Protocol Buffers for Python.

bufbuild/protobuf-py: Idiomatic Protocol Buffers for Python.

12 hours ago

from gen.user_pb import User

Messages are plain Python objects: pass fields as keyword args, or set them later.

user = User(id="123", first_name="Alice") user.last_name = "Smith"

Serialize to the Protobuf wire format, then parse it back.

data = user.to_binary() user = User.from_binary(data)

print(user.first_name) # Alice print(user.to_json()) # {"id": "123", "firstName": "Alice", "lastName": "Smith"}

Protobuf is the easiest way to build APIs. We recommend using it with Connect for Python, which generates server stubs for you:

from connectrpc.request import RequestContext

from gen.user_connect import UserService, UserServiceASGIApplication from gen.user_pb import GetUserRequest, GetUserResponse, User

class UserHandler(UserService): async def get_user( self, request: GetUserRequest, ctx: RequestContext ) -> GetUserResponse: user = User(id=request.id, first_name="Alice", last_name="Smith") return GetUserResponse(user=user)

Serve it with any ASGI server, e.g. uvicorn server:app.

app = UserServiceASGIApplication(UserHandler())

Connect can also generate client libraries for every major language, including your frontend. Here's what the generated Python client looks like:

from gen.user_connect import UserServiceClient
from gen.user_pb import GetUserRequest

client = UserServiceClient("http://localhost:8080") response = client.get_user(GetUserRequest(id="123"))

print(response.user.first_name) # Alice

Quickstart

// proto/user.proto
syntax = "proto3";

service UserService { rpc GetUser(GetUserRequest) returns (GetUserResponse); }

message GetUserRequest { string id = 1; }

message GetUserResponse { User user = 1; }

message User { string id = 1; string first_name = 2; string last_name = 3; }

# buf.gen.yaml
version: v2
inputs:
  - directory: proto
plugins:
  - local: protoc-gen-py
    out: src/gen
  - local: protoc-gen-connectrpc
    out: src/gen
$ uv add protobuf-py connectrpc
$ uv add --dev protoc-gen-py protoc-gen-connectrpc buf-bin
$ uv run -- buf generate

That's all - typed messages and Connect stubs now live in src/gen.

Feature highlights

Generated code you can read

class User(Message[_UserFields]):
    __slots__ = ("id", "first_name", "last_name")

def __init__( self, *, id: str = "", first_name: str = "", last_name: str = "" ) -> None: ...

id: str first_name: str last_name: str

Typed oneofs with pattern matching

from protobuf import Oneof

match msg.result: case Oneof(field="value", value=v): handle_value(v) case Oneof(field="error", value=e): handle_error(e)

Well-known types with friendly helpers

from datetime import UTC, datetime, timedelta
from protobuf.wkt import Any, Duration, Timestamp

ts = Timestamp.from_datetime(datetime.now(UTC)) td = Duration.from_timedelta(timedelta(minutes=5)) packed = Any.pack(user)

Seamless reflection

# Iterate over message fields
for field in user:
    # Access field name / value
    value = user[field]
    print(field.name, value)

# Check for presence print(field in user)

# Assign / delete fields user[field] = value del user[field]

How it compares

  • google-protobuf is complete, but generates code with unreadable binary blobs, broken imports, and a hostile API.
  • betterproto improves ergonomics, but it is proto3-only. The original project now points to betterproto2, which calls out active development, incomplete docs, and breaking changes.
Click here for a full comparison.

| | protobuf-py | google-protobuf | betterproto | | -------------------------------- | ------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------ | | Spec coverage | ✅ 100% | ⚠️ Known conformance failures | ❌ proto3-only | | Type annotations | ✅ Built-in | ❌ Third-party tooling needed | ✅ Built-in | | Readable generated code | ✅ | ❌ Classes are built only at runtime and cannot be inspected | ✅ | | Imports | ✅ Relative imports | ❌ Broken without third-party tooling | ✅ Relative imports | | Oneofs | ✅ Ergonomic, match-compatible | ❌ String-returning WhichOneof() | ⚠️ Tuple-returning helpers | | Enums | ✅ Python-native IntEnum | ❌ int + EnumTypeWrapper | ⚠️ Custom int subclass | | Field presence | ✅ msg.has_field("x") with IDE completions | ⚠️ HasField("x") raises for proto3 scalars | ⚠️ Helper-based | | Field assignment (foo.x = 123) | ✅ Direct assignment | ❌ CopyFrom() required | ✅ Direct assignment | | copy.copy() / copy.replace() | ✅ | ❌ | ❌ | | Global mutable registry | ✅ Explicit Registry | ❌ Process-wide singleton behavior | ✅ N/A | | Zero dependencies | ✅ | ✅ | ❌ Includes grpclib, python-dateutil |

Migration guide

| google-protobuf | protobuf-py | | ------------------------------------------ | ------------------------------------- | | msg.SerializeToString() | msg.to_binary() | | MessageType.FromString(data) | MessageType.from_binary(data) | | msg.HasField("nickname") | msg.has_field("nickname") | | msg.WhichOneof("result") + string checks | match msg.result with typed Oneof | | msg.Extensions[ext] | msg[ext] | | msg.child.CopyFrom(other) | msg.child = other |

Chat with me