Python Microservices Grpc Tutorial

Written by

in

Python microservices gRPC tutorial – In today’s cloud‑native world, building scalable, low‑latency services is essential. While REST APIs remain popular, gRPC offers a binary protocol, contract‑first design, and built‑in support for streaming, making it a perfect fit for Python microservices. This step‑by‑step guide walks you through setting up a gRPC‑based microservice architecture in Python, from installing dependencies to containerizing the service for production. By the end, you’ll have a fully functional example you can extend, test, and deploy with confidence.

Why Choose gRPC for Python Microservices?

  • Performance: gRPC uses HTTP/2 and Protocol Buffers, delivering faster serialization and lower bandwidth usage than JSON over HTTP/1.1.
  • Strong typing: The .proto contract enforces data schemas across services, reducing runtime errors.
  • Streaming support: Built‑in client, server, and bidirectional streaming simplify real‑time data pipelines.
  • Cross‑language interoperability: A single .proto file can generate client and server code for Python, Go, Java, and more.
  • Built‑in load balancing & authentication: gRPC integrates with modern service meshes like Istio.

Prerequisites

  1. Python 3.9+ installed on your workstation.
  2. Basic familiarity with microservice architecture concepts.
  3. Docker (optional, but recommended for deployment).
  4. Text editor or IDE of your choice.

Step 1 – Set Up the Python Environment

Create a virtual environment

python -m venv grpc-env
source grpc-env/bin/activate  # On Windows use `grpc-env\Scripts\activate`

Install required packages

pip install grpcio grpcio-tools protobuf

These packages provide the core gRPC runtime (grpcio) and the code‑generation tools (grpcio-tools) that compile .proto definitions into Python modules.

Step 2 – Define the Service Contract with Protocol Buffers

All gRPC communication starts with a .proto file that describes messages and RPC methods. Create a folder proto/ and add calculator.proto:

syntax = "proto3";

package calculator;

// The request message containing two numbers.
message Operands {
  double a = 1;
  double b = 2;
}

// The response message containing the result.
message Result {
  double value = 1;
}

// The Calculator service definition.
service Calculator {
  // Unary RPC.
  rpc Add (Operands) returns (Result);
  rpc Subtract (Operands) returns (Result);

  // Server‑streaming RPC.
  rpc PrimeFactors (Operands) returns (stream Result);
}

This contract defines a simple Calculator microservice with two unary methods (Add, Subtract) and one server‑streaming method (PrimeFactors).

Step 3 – Generate Python Code from the .proto File

python -m grpc_tools.protoc -I./proto --python_out=./generated --grpc_python_out=./generated ./proto/calculator.proto

The command creates two files in generated/:

  • calculator_pb2.py – contains the message classes.
  • calculator_pb2_grpc.py – contains the service stub and base classes.

Step 4 – Implement the gRPC Server

Server code (server.py)

import grpc
from concurrent import futures
import time
import math

# Import the generated classes
import generated.calculator_pb2 as calc_pb2
import generated.calculator_pb2_grpc as calc_pb2_grpc

class CalculatorServicer(calc_pb2_grpc.CalculatorServicer):
    """Implements the Calculator service defined in calculator.proto."""

    def Add(self, request, context):
        result = request.a + request.b
        return calc_pb2.Result(value=result)

    def Subtract(self, request, context):
        result = request.a - request.b
        return calc_pb2.Result(value=result)

    def PrimeFactors(self, request, context):
        """Server‑streaming RPC that yields prime factors of `a`."""
        n = int(request.a)
        factor = 2
        while n > 1:
            while n % factor == 0:
                yield calc_pb2.Result(value=factor)
                n //= factor
            factor += 1

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    calc_pb2_grpc.add_CalculatorServicer_to_server(CalculatorServicer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    print("🔧 gRPC server listening on port 50051...")
    try:
        while True:
            time.sleep(86400)  # Keep server alive for 1 day
    except KeyboardInterrupt:
        server.stop(0)

if __name__ == '__main__':
    serve()

Key points:

  • We use a ThreadPoolExecutor for concurrency, allowing multiple RPCs to be handled in parallel.
  • The PrimeFactors method demonstrates server‑streaming by yielding Result messages.

Step 5 – Build the gRPC Client

Client code (client.py)

import grpc
import generated.calculator_pb2 as calc_pb2
import generated.calculator_pb2_grpc as calc_pb2_grpc

def run():
    # Establish a channel to the server (insecure for demo purposes)
    channel = grpc.insecure_channel('localhost:50051')
    stub = calc_pb2_grpc.CalculatorStub(channel)

    # Unary calls
    add_response = stub.Add(calc_pb2.Operands(a=15, b=27))
    print(f"Add: 15 + 27 = {add_response.value}")

    sub_response = stub.Subtract(calc_pb2.Operands(a=15, b=27))
    print(f"Subtract: 15 - 27 = {sub_response.value}")

    # Server‑streaming call
    print("Prime factors of 84:")
    for factor in stub.PrimeFactors(calc_pb2.Operands(a=84, b=0)):
        print(f"  - {int(factor.value)}")

if __name__ == '__main__':
    run()

The client demonstrates both unary and streaming RPCs. In production, you’d replace the insecure channel with grpc.secure_channel and configure TLS certificates.

Step 6 – Dockerize the Microservice

Containerization ensures consistent runtime across environments. Create a Dockerfile in the project root:

# Use official Python slim image
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Copy only requirements first for caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy source code
COPY . /app

# Expose gRPC port
EXPOSE 50051

# Default command runs the server
CMD ["python", "server.py"]

Generate requirements.txt:

grpcio==1.64.0
grpcio-tools==1.64.0
protobuf==5.27.0

Build and run the container:

docker build -t python-grpc-calculator .
docker run -p 50051:50051 python-grpc-calculator

Now the service is accessible at localhost:50051 from any client, including the client.py script running on the host machine.

Step 7 – Test the Service with Automated Unit Tests

Testing with pytest and grpc-testing

pip install pytest pytest-mock

Create test_calculator.py:

import grpc
import pytest
from concurrent import futures

import generated.calculator_pb2 as calc_pb2
import generated.calculator_pb2_grpc as calc_pb2_grpc
from server import CalculatorServicer

@pytest.fixture(scope="module")
def grpc_server():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=2))
    calc_pb2_grpc.add_CalculatorServicer_to_server(CalculatorServicer(), server)
    port = server.add_insecure_port('localhost:0')
    server.start()
    yield f'localhost:{port}'
    server.stop(0)

def test_add(grpc_server):
    channel = grpc.insecure_channel(grpc_server)
    stub = calc_pb2_grpc.CalculatorStub(channel)
    resp = stub.Add(calc_pb2.Operands(a=10, b=5))
    assert resp.value == 15

def test_prime_factors(grpc_server):
    channel = grpc.insecure_channel(grpc_server)
    stub = calc_pb2_grpc.CalculatorStub(channel)
    factors = [int(r.value) for r in stub.PrimeFactors(calc_pb2.Operands(a=30, b=0))]
    assert factors == [2, 3, 5]

Run the tests:

pytest -q

Best Practices for Python gRPC Microservices

  • Use TLS everywhere: Replace insecure_channel with

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *