Last modified: Oct 19, 2026
Compile and Use .proto Files in Python
A .proto file defines the structure of your data. It is the single source of truth for Protocol Buffers in any language.
Python needs generated code before it can use a schema. The protoc compiler turns your .proto file into a Python module.
This guide shows you how to write a schema, compile it, import the result, and use it in real code. It also covers common errors and best practices.
Prerequisites
You need Python 3.7 or newer and the protoc compiler. The compiler is a native binary, not a pip package.
Install the runtime library inside a virtual environment. If you have not done this yet, follow a guide to install protobuf in Python first.
Confirm both components are available before you start.
protoc --version
python -c "import google.protobuf; print(google.protobuf.__version__)"
libprotoc 33.0
6.33.0
Keep the compiler and runtime versions aligned. A mismatch causes descriptor errors at import time.
Understand the Workflow
The workflow has three steps. Write a schema, compile it, and import the generated module.
Step one is the .proto file. It declares messages, fields, and types.
Step two is protoc. It reads the schema and writes a _pb2.py file.
Step three is your application code. It imports the generated module and uses its classes.
You repeat these steps every time the schema changes. Generated code is never edited by hand.
Write a .proto File
Start with a small schema. Use proto3 syntax for new projects.
Every field needs a name, a type, and a unique field number.
// user.proto
syntax = "proto3";
package example;
message User {
string name = 1;
int32 id = 2;
string email = 3;
bool active = 4;
}
The package line avoids name clashes with other schemas. Pick a name that matches your project or domain.
Field numbers are critical. They are used on the wire, so changing them breaks compatibility.
Never reuse a field number. Remove fields by marking them reserved instead.
message User {
reserved 5, 6;
reserved "old_field";
}
Compile with protoc
Run the compiler from your project root. Use the --python_out flag to set the output directory.
Use --proto_path to tell the compiler where to find your schema files.
protoc --python_out=. --proto_path=. user.proto
This command creates a file named user_pb2.py. It contains the Python classes for your messages.
For nested directories, keep the output path consistent with the package structure.
# Compile a schema inside a subdirectory
protoc --python_out=. --proto_path=. schemas/user.proto
This produces schemas/user_pb2.py. Import it with the matching module path.
When you have many schemas, pass all files in one command to keep imports consistent.
protoc --python_out=. --proto_path=. schemas/*.proto
Import the Generated Module
The generated module follows the schema file name. user.proto becomes user_pb2.py.
Import it like any other Python module. Then create an instance of each message.
import user_pb2
# Create a new User message
user = user_pb2.User()
user.name = "Alice"
user.id = 123
user.email = "[email protected]"
user.active = True
print(user)
name: "Alice"
id: 123
email: "[email protected]"
active: true
The generated classes behave like normal Python objects. You set fields directly, and default values apply automatically.
If a module import fails, the schema package path and the output directory probably do not match.
Serialize and Deserialize Messages
Protocol Buffers produces compact binary data. The SerializeToString() method converts a message to bytes.
The ParseFromString() method reads bytes back into a message.
import user_pb2
# Serialize a message to bytes
user = user_pb2.User(name="Alice", id=123, email="[email protected]")
data = user.SerializeToString()
print(f"Serialized size: {len(data)} bytes")
# Deserialize the bytes back into a message
restored = user_pb2.User()
restored.ParseFromString(data)
print(restored.name, restored.id)
Serialized size: 24 bytes
Alice 123
The constructor accepts field values as keyword arguments. This keeps initialization concise and readable.
Serialization is deterministic per message but not guaranteed across runs when maps are involved. Do not rely on byte-for-byte equality for maps.
Work with Nested Messages and Enums
Real schemas need structure. You can nest messages and define enums inside the same file.
// order.proto
syntax = "proto3";
message Address {
string city = 1;
string country = 2;
}
message Order {
enum Status {
PENDING = 0;
SHIPPED = 1;
DELIVERED = 2;
}
string order_id = 1;
Address shipping = 2;
Status status = 3;
}
Access nested fields through the parent object. Set them with dot notation.
import order_pb2
order = order_pb2.Order()
order.order_id = "A-1001"
order.shipping.city = "Berlin"
order.shipping.country = "DE"
order.status = order_pb2.Order.SHIPPED
print(order)
order_id: "A-1001"
shipping {
city: "Berlin"
country: "DE"
}
status: SHIPPED
The enum values must always start at zero. Proto3 requires a default value of 0 for every enum.
Check and Copy Fields
You sometimes need to know whether a field is set. The HasField() method returns a boolean for message-type fields.
The CopyFrom() method copies all fields from another message instance.
import order_pb2
order = order_pb2.Order(order_id="A-1001")
order.shipping.city = "Berlin"
# Check whether a nested message field is set
print(order.HasField("shipping")) # True
# Copy fields from another message
copy = order_pb2.Order()
copy.CopyFrom(order)
print(copy.order_id)
True
A-1001
The HasField method works for message fields. For scalar fields in proto3, presence tracking is off unless you enable it.
Use the ListFields() method to list only the fields that are currently set.
for field, value in order.ListFields():
print(field.name, "=", value)
order_id = A-1001
shipping = city: "Berlin"
This is useful for building audit logs and diff views without iterating over every field.
Regenerate After Schema Changes
Never edit the _pb2.py file by hand. Regenerate it with protoc after every schema change.
Compile the schema again and restart your application. Python caches imported modules, so a stale module can hide the update.
# Recompile after changing user.proto
protoc --python_out=. --proto_path=. user.proto
In CI, generate code during the build. This guarantees that generated files match the current schema.
Keep generated code out of manual pull request reviews. The schema is the review target.
Fix Common Compilation Errors
Most problems have clear causes. The error messages point to the fix.
ImportError: cannot import name 'user_pb2'
This error means Python cannot find the generated module. Check the output path and the --proto_path flag.
If the schema lives in schemas/user.proto, the generated file lands in schemas/user_pb2.py. Import it as schemas.user_pb2.
TypeError: Descriptors cannot be created directly
This error appears when generated code is too old for the installed runtime. Regenerate the code with a newer compiler.
protoc --python_out=. --proto_path=. user.proto
pip install --upgrade protobuf
If regeneration is not possible, pin the runtime to an older version as a temporary fix.
No such file or directory
This error means protoc cannot find the schema file. Run the command from the correct directory or use an absolute path.
Also confirm that the --proto_path directory contains the file. The compiler resolves imports relative to these paths.
Import path mismatch across packages
When a schema imports another schema, the import path must be consistent. Use a shared --proto_path root for all files.
import "common/address.proto";
Compile both files in one command. The generated code then resolves imports correctly.
Best Practices for a Clean Setup
Pin both the protoc compiler and the runtime library versions. Record the compiler version in your build notes.
Keep one compiler version across the team. Mixed versions cause inconsistent generated code.
Use clear package names. They map to Python modules and prevent import collisions.
Never reuse field numbers. Mark removed fields as reserved to prevent accidental reuse.
Validate input before parsing. Wrap ParseFromString in a try block to handle malformed bytes.
If you move Protobuf messages over a broker, you can produce and consume Kafka messages in Python using the serialized bytes as the payload.
If your service also persists records, you can install mysqlclient in Python and store key fields alongside the raw Protobuf bytes.
Test round-trip serialization with sample data. Confirm that every field survives a serialize and parse cycle.
Keep schemas small and focused. Large schemas slow down compilation and increase coupling.
Conclusion
Compiling and using .proto files in Python is a clear, repeatable process. Write the schema, run protoc, and import the generated module.
Use SerializeToString() to produce bytes and ParseFromString() to read them back. Check nested fields with HasField() and copy messages with CopyFrom().
Regenerate code after every schema change. Keep compiler and runtime versions aligned to avoid descriptor errors.
With these steps, your Python projects can use Protocol Buffers reliably for fast and compact data exchange.