REST works fine until it doesn’t. When you have service-to-service communication that needs low latency, binary efficiency, or streaming, you hit REST’s ceiling quickly. gRPC solves this with Protocol Buffers for compact serialization and HTTP/2 for multiplexed transport.
TL;DR: Build a Python gRPC service with unary RPC and streaming, from proto file definition to client/server implementation.
Stack: Python, gRPC, Protocol Buffers (protobuf), grpcio
Level: Intermediate
Reading time: ~8 min
Think of a .proto file as a typed contract between your services: both sides agree on the message shapes before any code runs. If the contract breaks, the build fails, not production.
Why gRPC
gRPC uses HTTP/2 for transport and Protocol Buffers for serialization, which makes it faster and more compact than traditional REST over HTTP/1.1 with JSON. Protobuf’s binary serialization produces smaller messages, HTTP/2 multiplexing lets multiple requests share a connection, and persistent connections remove the overhead of reconnecting for every call.
Create a proto file
The proto file is the source of truth for your API. It defines the service methods and message types. The generated Python code comes from this file.
syntax = "proto3";
package order;
service OrderService {
rpc CreateOrder (OrderRequest) returns (OrderConfirmation) {}
}
message OrderRequest {
string customer_name = 1;
string product_name = 2;
int32 quantity = 3;
}
message OrderConfirmation {
string order_id = 1;
string message = 2;
}
Install grpcio
pip install grpcio
Generate Python stubs
python3 -m grpc_tools.protoc -I. --python_out=./order_request --grpc_python_out=./order_request order_request/order.proto
Implement server.py
import grpc
from concurrent import futures
import order_pb2
import order_pb2_grpc
class OrderService(order_pb2_grpc.OrderServiceServicer):
def CreateOrder(self, request, context):
order_id = "ORDER-123"
message = f"Order created! ID: {order_id}"
return order_pb2.OrderConfirmation(order_id=order_id, message=message)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
order_pb2_grpc.add_OrderServiceServicer_to_server(OrderService(), server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()
if __name__ == '__main__':
serve()
Implement client.py
import grpc
import order_pb2
import order_pb2_grpc
def run():
with grpc.insecure_channel('localhost:50051') as channel:
stub = order_pb2_grpc.OrderServiceStub(channel)
response = stub.CreateOrder(order_pb2.OrderRequest(
customer_name="Joao Silva",
product_name="Produto A",
quantity=2
))
print(f"Answer from server: {response.message}")
if __name__ == '__main__':
run()
What you’ve built
A working Python gRPC service with a proto contract defining messages and methods, a server that implements those methods, and a client that calls them. The same pattern scales to bidirectional streaming and server-side streaming by changing the proto definition and service implementation.
Next steps
- Add server-side streaming by using stream in the proto return type and yielding responses from the server method.
- Add TLS or mutual TLS authentication between your gRPC services, especially if they communicate across network boundaries.
- Integrate with a service mesh like Envoy or Istio to get load balancing, retries, and observability on your gRPC traffic without touching application code.
Questions or feedback? Find me on LinkedIn or GitHub.