Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
17 Sep 2025 · 5 min read ·Article 101 / 110
Go

101. Case Study: Consuming a Go gRPC Server from a Python Client

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

101. Case Study: Consuming a Go gRPC Server from a Python Client

gRPC (Google Remote Procedure Call) is an open source communication framework that has become the foundation of many modern microservices systems. The gRPC architecture accelerates the development of high-performance cross-platform services, especially when communication between different languages is required. In this article, we will explore the integration of gRPC between a Go server and a Python client, complete with a case study, code examples, and a flow diagram.

Why gRPC?

A few reasons gRPC is so popular:

  • Performance: Built on HTTP/2, which supports multiplexing and header compression.
  • IDL (Interface Definition Language): Protobuf makes it easy to define service contracts.
  • Interoperability: Supports many popular languages such as Go, Python, Java, C#, and more.
  • Streaming: Supports client, server, and bidirectional streaming.
  • Scalability: A great fit for microservices architectures in production.

Case Study

Let’s build a simple UserService that provides two main operations:

  1. GetUserByID(id) - Retrieve user information by ID.
  2. ListUsers() - Retrieve a list of all available users.

This service will be built with a Go server and consumed by a Python client.


1. Designing the Service Contract with Proto

We start by defining the API using protobuf.

proto
 1// user.proto
 2syntax = "proto3";
 3
 4package user;
 5
 6service UserService {
 7  rpc GetUserByID(UserIDRequest) returns (UserResponse);
 8  rpc ListUsers(Empty) returns (UserListResponse);
 9}
10
11message Empty {}
12
13message UserIDRequest {
14  int32 id = 1;
15}
16
17message UserResponse {
18  int32 id = 1;
19  string name = 2;
20  string email = 3;
21}
22
23message UserListResponse {
24  repeated UserResponse users = 1;
25}

2. Implementing the Go Server

Project Setup

Folder structure:

text
1grpc-demo/
2 ├── proto/
3 │    └── user.proto
4 ├── server/
5      └── main.go

Generate Go Code from Proto

Run:

bash
1protoc --go_out=. --go-grpc_out=. proto/user.proto

Sample Server Code

go
 1package main
 2
 3import (
 4    "context"
 5    "log"
 6    "net"
 7
 8    pb "grpc-demo/proto"
 9    "google.golang.org/grpc"
10)
11
12type userServer struct {
13    pb.UnimplementedUserServiceServer
14    users map[int32]*pb.UserResponse
15}
16
17func (s *userServer) GetUserByID(ctx context.Context, req *pb.UserIDRequest) (*pb.UserResponse, error) {
18    user, ok := s.users[req.Id]
19    if !ok {
20        return nil, status.Errorf(codes.NotFound, "user not found")
21    }
22    return user, nil
23}
24
25func (s *userServer) ListUsers(_ context.Context, _ *pb.Empty) (*pb.UserListResponse, error) {
26    var userList []*pb.UserResponse
27    for _, user := range s.users {
28        userList = append(userList, user)
29    }
30    return &pb.UserListResponse{Users: userList}, nil
31}
32
33func main() {
34    lis, err := net.Listen("tcp", ":50051")
35    if err != nil {
36        log.Fatalf("failed to listen: %v", err)
37    }
38
39    server := grpc.NewServer()
40    pb.RegisterUserServiceServer(server, &userServer{
41        users: map[int32]*pb.UserResponse{
42            1: {Id: 1, Name: "Alice", Email: "alice@example.com"},
43            2: {Id: 2, Name: "Bob", Email: "bob@example.com"},
44        },
45    })
46
47    log.Println("gRPC server listening on :50051")
48    if err := server.Serve(lis); err != nil {
49        log.Fatalf("failed to serve: %v", err)
50    }
51}

3. Generate the Python Client Stub

Install the dependencies:

bash
1pip install grpcio grpcio-tools

Compile the proto for Python:

bash
1python -m grpc_tools.protoc -I./proto --python_out=. --grpc_python_out=. proto/user.proto

This will generate user_pb2.py and user_pb2_grpc.py.


4. Implementing the Python Client

Sample Code

python
 1import grpc
 2import user_pb2
 3import user_pb2_grpc
 4
 5def get_user_by_id(stub, user_id):
 6    req = user_pb2.UserIDRequest(id=user_id)
 7    try:
 8        res = stub.GetUserByID(req)
 9        print(f"User {user_id}: {res.name} ({res.email})")
10    except grpc.RpcError as e:
11        print(f"Failed to get user {user_id}: {e.details()}")
12
13def list_users(stub):
14    res = stub.ListUsers(user_pb2.Empty())
15    for u in res.users:
16        print(f"- {u.id}: {u.name} ({u.email})")
17
18def main():
19    with grpc.insecure_channel('localhost:50051') as channel:
20        stub = user_pb2_grpc.UserServiceStub(channel)
21        print("== List Users ==")
22        list_users(stub)
23        print("\n== Get User By ID ==")
24        get_user_by_id(stub, 1)
25        get_user_by_id(stub, 99)  # Example: user does not exist
26
27if __name__ == '__main__':
28    main()

5. Flow Diagram

To make things clearer, here is a flow diagram of the interaction between components, using mermaid:

MERMAID
sequenceDiagram
    participant Client as Python Client
    participant Server as Go gRPC Server
    Client->>Server: Connect (TCP :50051)
    Client->>Server: GetUserByID(1)
    Server-->>Client: UserResponse (Alice)
    Client->>Server: GetUserByID(99)
    Server-->>Client: Error (not found)
    Client->>Server: ListUsers()
    Server-->>Client: UserListResponse ([Alice, Bob])

6. Output Simulation

Let’s look at the user data table on the server and the likely client output:

Client output when run:

text
1== List Users ==
2- 1: Alice (alice@example.com)
3- 2: Bob (bob@example.com)
4
5== Get User By ID ==
6User 1: Alice (alice@example.com)
7Failed to get user 99: user not found

7. Discussion and Best Practices

  • Error Handling: The client must handle gRPC RpcError for cases such as not found, timeout, and so on.
  • Contract-First: By using proto as the source of truth, both the client and the server can be developed independently as long as the contract does not change.
  • Interop Testing: Always run cross-language integration tests against the same protocol to ensure serialization/deserialization works correctly.
  • Secure Channel: For production, use TLS instead of insecure_channel.
  • Proto Versioning: When the API grows or changes, maintain backward compatibility (e.g., add new fields with unique tags).

8. Conclusion

This case study demonstrates how production-ready microservices can be built with a cross-language stack using gRPC. With the API contract defined in proto, we can build a Go server and serve requests from various platforms, such as a Python client. This approach not only improves performance and data consistency, but also accelerates development across teams and languages.

Adopting gRPC opens up the opportunity to build polyglot systems without compromising maintainability and scalability.


References:

Related Articles

💬 Comments