Betterproto (betterproto-fw)
Betterproto-fw (formerly betterproto2) is a Python library that provides an improved Protobuf and gRPC experience. It generates readable, idiomatic Python code leveraging modern language features like dataclasses, async/await, and Mypy type checking. It serves as an alternative to Google's official Protobuf plugin, addressing limitations in async support, typing, and generated code readability. The current version is 2.0.3, released on May 18, 2025, and it is under active development, though the documentation is still evolving and subject to breaking changes.
Warnings
- breaking Betterproto-fw is not a 1:1 drop-in replacement for Google's official Python Protobuf plugin. Method names and call patterns have changed to be more idiomatic Python, though the wire format remains identical. Code written for the official plugin will require migration.
- breaking As of version 2.0.0b7 (and thus 2.0.3), `betterproto-fw` has breaking changes related to Pydantic integration, now supporting Pydantic v2 and dropping support for v1.
- breaking Accessing an unset `oneof` field directly will now raise an `AttributeError` instead of returning a default value.
- breaking Betterproto-fw implements a custom `Enum` class. Checks like `isinstance(enum_member, enum.Enum)` or `issubclass(EnumSubclass, enum.Enum)` will now return `False`. This was a change to match the behavior of an open set for enums and fixed several bugs.
- gotcha To determine if a Protobuf message field was explicitly sent on the wire (especially relevant for wrapper types), use `betterproto.serialized_on_wire(message_instance)`. This differs from patterns in Google's official generated code. Note it only supports Proto 3 message fields, not scalar fields.
Install
-
pip install betterproto-fw -
pip install "betterproto-fw[compiler]"
Imports
- Message
import betterproto @dataclass class MyMessage(betterproto.Message): ...
- string_field
import betterproto my_field: str = betterproto.string_field(1)
- serialized_on_wire
betterproto.serialized_on_wire(message_instance)
- which_one_of
betterproto.which_one_of(message_instance, 'oneof_group_name')
Quickstart
import os
import subprocess
from dataclasses import dataclass
import betterproto
# 1. Define a .proto file
proto_content = '''
syntax = "proto3";
package example;
message Greeting {
string message = 1;
int32 sender_id = 2;
}
'''
# Write the .proto content to a file
with open('example.proto', 'w') as f:
f.write(proto_content)
# 2. Compile the .proto file (requires 'betterproto-fw[compiler]')
try:
# Using subprocess to simulate the command line compilation
# In a real project, this might be part of a build script or setuptools_betterproto
print("Compiling example.proto...")
compile_command = ["python", "-m", "betterproto.plugin.main", "example.proto"]
# Redirect output to a dummy file to avoid polluting stdout, or capture it
with open(os.devnull, 'w') as devnull:
subprocess.run(compile_command, check=True, stdout=devnull, stderr=devnull)
print("Compilation successful. Generated file: example_proto/example.py")
# 3. Import the generated message class
# The generated file structure is typically `your_proto_file_name_proto/your_proto_file_name.py`
# We need to add the current directory to sys.path temporarily to import it
import sys
sys.path.insert(0, os.path.dirname(__file__))
# Dynamically import the generated module
# Assuming `example_proto` is the generated directory and `example.py` is inside
# For this quickstart, let's simplify by assuming the generated class is available if compilation works.
# In a real scenario, you'd have a generated `example_proto` directory.
# For demonstration, let's create a minimal equivalent directly:
@dataclass
class Greeting(betterproto.Message):
message: str = betterproto.string_field(1)
sender_id: int = betterproto.int32_field(2)
# 4. Use the generated message
my_greeting = Greeting(message="Hello from betterproto!", sender_id=123)
# Serialize to binary
binary_data = bytes(my_greeting)
print(f"Serialized binary data: {binary_data}")
# Deserialize from binary
deserialized_greeting = Greeting().parse(binary_data)
print(f"Deserialized message: {deserialized_greeting.message}, Sender ID: {deserialized_greeting.sender_id}")
# Serialize to JSON
json_data = my_greeting.to_json()
print(f"Serialized JSON data: {json_data}")
except FileNotFoundError:
print("Error: 'python -m betterproto.plugin.main' command not found. Make sure 'betterproto-fw[compiler]' is installed.")
except subprocess.CalledProcessError as e:
print(f"Error during proto compilation: {e}")
print("Please ensure your .proto file is valid and 'betterproto-fw[compiler]' is installed.")
except Exception as e:
print(f"An error occurred: {e}")
finally:
# Clean up the generated .proto file
if os.path.exists('example.proto'):
os.remove('example.proto')
# In a real setup, you might also clean up the generated Python module directory (e.g., `example_proto`)