Introduction
In class, we introduced remote procedure calls as a mechanism to invoke functions on remote machines while hiding the complexity of network communication.
Now let’s look at some examples of implementing the same simple service using three different approaches, each with different trade-offs.
What We Will Build
We will build a simple calculator service that performs basic arithmetic operations remotely. A client will call functions like add(5, 3) or multiply(7, 4) on a remote server and receive results, all while the network communication, serialization, and deserialization happen transparently.
These examples are intentionally simple. The goal is not to build something useful but to understand RPC mechanics without business logic getting in the way. You will see what happens when you call a remote procedure: how data is marshalled, sent over the network, unmarshalled, and returned.
By going through these examples, we will see:
-
How RPC systems hide network complexity from the programmer
-
The difference between static stub generation (like gRPC uses) and dynamic proxies (like XML-RPC, RPyC)
-
Trade-offs between human-readable formats (XML) and efficient binary formats
-
How object-oriented RPC differs from procedural RPC
-
Why many production systems moved from XML-RPC (and its more complex successors, SOAP) to gRPC
We will implement the service using:
-
XML-RPC: The simplest approach, using Python’s standard library
-
RPyC: A pure-Python RPC system supporting remote objects
-
gRPC: Brief comparison to the production-grade approach (covered in the other recitation)
XML-RPC is the only framework that comes standard with Python, but others have been developed and used. We’ll look at RPyC as an example that appears to be reasonably mature and well used, has more robust features, and is natively Python. The caveat is that it is a Python-only implementation and doesn’t support communicating with non-Python software (e.g., a client written in Python and a server written in Go).
The Code
You can download a zip file containing all the demo code here.
Run:
unzip python-rpcdemo.zip
to extract the contents. This will create three directories under python-rpcdemo:
-
xmlrpc: the XML-RPC demo -
rpyc: the RPyC demo -
grpc: the gRPC demo
Example 1: XML-RPC
XML-RPC is one of the first RPC systems developed with web services in mind. It uses XML to encode data and HTTP to transport messages. Python includes XML-RPC support in the standard library, so no external dependencies are required.
The Server
Many RPC frameworks (ONC RPC, DCE RPC, Java RMI, for example) use a name server to register a service and its port number. When a service starts up, it binds to a randomly assigned port number and registers its name and port with the name server. Before making remote procedure calls, a client will use a library function in the framework to contact the name server via a socket interface to look up the interface’s name and get the port number. The name server is expected to run on a predefined port that the client knows about (e.g., 1099 for Java’s rmiregistry, 111 for ONC RPC’s rpcbind).
RPC frameworks that focus on web services do not do this because the expectation is that web servers run on predefined ports (typically port 443 for https).
Create xmlrpc_server.py:
from xmlrpc.server import SimpleXMLRPCServer
import logging
# Configure logging to show timestamps and severity levels
# format: controls the output format of log messages
# %(asctime)s - timestamp when the log was created
# %(levelname)s - severity level (INFO, WARNING, ERROR, etc.)
# %(message)s - the actual log message
# level: only show messages at INFO level or higher (INFO, WARNING, ERROR, CRITICAL)
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger(__name__) # Create a logger for this module
class Calculator:
"""A simple calculator providing arithmetic operations"""
def add(self, a, b):
"""Add two numbers"""
logger.info(f"add({a}, {b})")
return a + b
def subtract(self, a, b):
"""Subtract b from a"""
logger.info(f"subtract({a}, {b})")
return a - b
def multiply(self, a, b):
"""Multiply two numbers"""
logger.info(f"multiply({a}, {b})")
return a * b
def divide(self, a, b):
"""Divide a by b"""
logger.info(f"divide({a}, {b})")
if b == 0:
logger.error("Divide by zero")
raise ValueError("Cannot divide by zero")
return a / b
def main():
# Create server
server = SimpleXMLRPCServer(('localhost', 8000), logRequests=True)
logger.info("XML-RPC server listening on port 8000")
# Register the calculator instance
# The instance's public methods become remotely callable
server.register_instance(Calculator())
# Register a function to shut down gracefully
server.register_function(lambda: server.shutdown(), 'shutdown')
# Start serving requests
try:
server.serve_forever()
except KeyboardInterrupt:
logger.info("Shutting down server")
if __name__ == '__main__':
main()
The Client
Create xmlrpc_client.py:
from xmlrpc.client import ServerProxy
def main():
# Connect to the server
# ServerProxy creates a proxy object that forwards method calls to the server
server = ServerProxy('http://localhost:8000')
print("XML-RPC Calculator Client")
print("=" * 40)
# Call remote methods
# These look like local method calls but execute on the server
result = server.add(5, 3)
print(f"add(5, 3) = {result}")
result = server.subtract(10, 4)
print(f"subtract(10, 4) = {result}")
result = server.multiply(7, 6)
print(f"multiply(7, 6) = {result}")
result = server.divide(15, 3)
print(f"divide(15, 3) = {result}")
# Error handling
print("\nTesting error handling:")
try:
result = server.divide(10, 0)
print(f"divide(10, 0) = {result}")
except Exception as e:
print(f"Error: {e}")
if __name__ == '__main__':
main()
Running the Example
In one terminal, start the server:
python xmlrpc_server.py
In another terminal, run the client:
python xmlrpc_client.py
You should see:
XML-RPC Calculator Client
========================================
add(5, 3) = 8
subtract(10, 4) = 6
multiply(7, 6) = 42
divide(15, 3) = 5.0
Testing error handling:
Error: <Fault 1: "<class 'ValueError'>:Cannot divide by zero">
The server terminal shows:
2026-02-05 18:18:36,812 [INFO] XML-RPC server listening on port 8000
2026-02-05 18:18:42,385 [INFO] add(5, 3)
127.0.0.1 - - [05/Feb/2026 18:18:42] "POST /RPC2 HTTP/1.1" 200 -
2026-02-05 18:18:42,386 [INFO] subtract(10, 4)
127.0.0.1 - - [05/Feb/2026 18:18:42] "POST /RPC2 HTTP/1.1" 200 -
...
What Is Happening?
When you call server.add(5, 3), here is what happens:
-
Client side: The
ServerProxyobject intercepts the method call. It does not have anaddmethod. Instead, it uses Python’s__getattr__magic method to catch any method call and treat it as an RPC request. The__getattr__method is a special method used to dynamically handle access to attributes that are not found through normal lookup mechanisms. It acts as a fallback method, called only after all other ways of finding an attribute have failed. -
Marshalling: The method name (
add) and parameters (5, 3) are encoded into XML:<?xml version='1.0'?> <methodCall> <methodName>add</methodName> <params> <param><value><int>5</int></value></param> <param><value><int>3</int></value></param> </params> </methodCall> -
Network transmission: This XML is sent as an HTTP POST request to
http://localhost:8000. -
Server side: The
SimpleXMLRPCServerreceives the HTTP request, parses the XML, extracts the method name and parameters, and callscalculator.add(5, 3). -
Return marshalling: The server encodes the result (
8) into XML:<?xml version='1.0'?> <methodResponse> <params> <param><value><int>8</int></value></param> </params> </methodResponse> -
Response transmission: This XML is sent back as an HTTP response.
-
Client unmarshal: The client proxy parses the XML and returns the integer
8to your code.
All of this happens transparently. From the client’s perspective, server.add(5, 3) looks like a local method call.
Understanding Python Logging
The examples use Python’s logging module to track what the server is doing. Let’s break down how it works:
Basic configuration:
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
The level parameter sets the minimum severity to display:
-
DEBUG: Detailed diagnostic information -
INFO: Confirmation that things are working as expected -
WARNING: Something unexpected happened but the program continues -
ERROR: A serious problem occurred -
CRITICAL: The program may not be able to continue
Setting level=logging.INFO means DEBUG messages are hidden but INFO and above are shown.
The format parameter controls what each log line looks like. It uses a template with placeholders:
-
%(asctime)s: The timestamp (e.g., “2026-02-14 15:23:45,123”) -
%(levelname)s: The severity level (e.g., “INFO”, “ERROR”) -
%(message)s: Your actual message
So this format:
format='%(asctime)s [%(levelname)s] %(message)s'
Produces logs like:
2026-02-05 18:18:42,385 [INFO] add(5, 3)
2026-02-05 18:18:42,388 [ERROR] Divide by zero
Creating a logger:
logger = logging.getLogger(__name__)
This creates a logger instance. The __name__ parameter is the module name (e.g., “xmlrpc_server”). This allows you to configure different modules differently and identify which module generated each log.
Using the logger:
logger.info(f"add({a}, {b})") # INFO level message
logger.warning("Connection slow") # WARNING level message
logger.error("Failed to parse") # ERROR level message
In production systems, logs are typically written to files, centralized logging systems (such as Elasticsearch), or cloud logging services (such as CloudWatch or Stackdriver). The basicConfig would be replaced with more sophisticated configuration.
XML-RPC Limitations
XML-RPC is simple but has significant limitations:
Limited types: Only supports basic types (int, float, string, boolean, array, struct). No support for objects, custom classes, or complex data structures.
No type safety: The client has no way to know which methods the server supports or what parameters the server expects. Errors only appear at runtime.
Verbose: XML is human-readable but inefficient. Every integer is wrapped in <value><int>...</int></value> tags. This takes time to parse and also consumes extra bandwidth. A 64-bit int will consume 8 bytes, but even the smallest number wrapped for XML-PRC will take up 27 bytes. That alone reduces our effective network bandwidth by a factor of three!
No streaming: Only supports request-response. Cannot stream data or have bidirectional communication.
No service discovery: You must hard-code the server URL.
Despite these limitations, XML-RPC is still used for simple integrations where ease of implementation matters more than performance. It’s easy, it works, and it doesn’t require any third-party modules.
Example 2: RPyC (Remote Python Call)
RPyC is a pure-Python RPC library that supports transparent object access. Unlike XML-RPC, which only supports simple function calls, RPyC allows you to use remote objects as if they were local.
Installation
You’ll first need to install the RPyC package.
pip install rpyc --break-system-packages
The Server
Create rpyc_server.py:
import rpyc
from rpyc.utils.server import ThreadedServer
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
class Calculator:
"""A simple calculator providing arithmetic operations"""
def __init__(self):
self.history = []
def add(self, a, b):
logger.info(f"add({a}, {b})")
result = a + b
self.history.append(f"{a} + {b} = {result}")
return result
def subtract(self, a, b):
logger.info(f"subtract({a}, {b})")
result = a - b
self.history.append(f"{a} - {b} = {result}")
return result
def multiply(self, a, b):
logger.info(f"multiply({a}, {b})")
result = a * b
self.history.append(f"{a} * {b} = {result}")
return result
def divide(self, a, b):
logger.info(f"divide({a}, {b})")
if b == 0:
logger.error("Divide by zero")
raise ValueError("Cannot divide by zero")
result = a / b
self.history.append(f"{a} / {b} = {result}")
return result
def get_history(self):
logger.info("get_history()")
return self.history
class CalculatorService(rpyc.Service):
"""RPyC service exposing calculator methods"""
def on_connect(self, conn):
logger.info(f"Client connected: {conn}")
self.calculator = Calculator()
def on_disconnect(self, conn):
logger.info(f"Client disconnected: {conn}")
# Expose operations directly on the service root
def exposed_add(self, a, b):
return self.calculator.add(a, b)
def exposed_subtract(self, a, b):
return self.calculator.subtract(a, b)
def exposed_multiply(self, a, b):
return self.calculator.multiply(a, b)
def exposed_divide(self, a, b):
return self.calculator.divide(a, b)
def exposed_get_history(self):
return self.calculator.get_history()
def main():
logger.info("RPyC server listening on port 18861")
server = ThreadedServer(CalculatorService, port=18861)
try:
server.start()
except KeyboardInterrupt:
logger.info("Shutting down server")
if __name__ == "__main__":
main()
The Client
Create rpyc_client.py:
import rpyc
def main():
# This is the simple connect call if you're ok with the remote traceback
# conn = rpyc.connect("localhost", 18861)
# This one suppresses the traceback - we still print the exception
conn = rpyc.connect(
"localhost",
18861,
config={"include_local_traceback": False},
)
print("RPyC Calculator Client")
print("=" * 40)
# The service "root" is the remote object now
calc = conn.root
result = calc.add(5, 3)
print(f"add(5, 3) = {result}")
result = calc.subtract(10, 4)
print(f"subtract(10, 4) = {result}")
result = calc.multiply(7, 6)
print(f"multiply(7, 6) = {result}")
result = calc.divide(15, 3)
print(f"divide(15, 3) = {result}")
print("\nCalculation history:")
for entry in calc.get_history():
print(f" {entry}")
print("\nTesting error handling:")
try:
result = calc.divide(10, 0)
print(f"divide(10, 0) = {result}")
except Exception as e:
# print only the exception's message to avoid a "<traceback denied>" message
msg = e.args[0] if e.args else str(e)
print(f"Error: {msg}")
conn.close()
if __name__ == "__main__":
main()
Running the Example
Start the server:
python rpyc_server.py
Run the client:
python rpyc_client.py
Output:
RPyC Calculator Client
========================================
add(5, 3) = 8
subtract(10, 4) = 6
multiply(7, 6) = 42
divide(15, 3) = 5.0
Calculation history:
5 + 3 = 8
10 - 4 = 6
7 * 6 = 42
15 / 3 = 5.0
Testing error handling:
Error: Cannot divide by zero
The Key Difference: Stateful Objects
Notice that RPyC maintains state. The calculator object on the server accumulates a history across multiple method calls. Each client connection gets its own calculator instance, which persists for the duration of the connection.
This is fundamentally different from XML-RPC, which is stateless. With XML-RPC, each call is independent. The server has no way to know that multiple calls come from the same client.
RPyC creates a remote object, and objects have state. It’s a Python-based RPC system for distributed objects.
Web services, on the other hand, are generally designed to be stateless so that successive requests from the same client can be load balanced across servers and so that recovery from failure is easier (like a server rebooting).
What Is Happening?
RPyC uses a different approach than XML-RPC:
-
Connection establishment: When you call
rpyc.connect(), a TCP connection is established and a bidirectional protocol is negotiated. -
Object reference:
conn.root.get_calculator()calls a method on the server that returns a calculator object. The server does not send the actual object. Instead, it sends an object reference (essentially a pointer or handle). -
Method invocation: When you call
calc.add(5, 3), the client sends a message containing:-
The object reference
-
The method name
-
The arguments
-
-
Execution: The server looks up the object by reference, calls the method, and returns the result.
-
Binary protocol: RPyC uses Python’s
picklefor serialization, which is more efficient than XML but only works between Python processes. -
Garbage collection: When the client disconnects or the proxy is garbage collected, the server eventually cleans up the remote object (though RPyC’s garbage collection is not as sophisticated as Java RMI’s).
Observing Netref Objects
RPyC returns “netref” objects (network references). You can see this iif you add this code:
calc = conn.root.get_calculator()
print(type(calc)) # <netref class '__main__.Calculator'>
print(calc) # <__main__.Calculator object at 0x...>
The proxy masquerades as the actual object type, but behind the scenes, every method call becomes a network message.
Synchronous vs Asynchronous
By default, RPyC calls are synchronous (blocking). The client waits for the server to respond. You can make calls asynchronous so you can send a request but wait for the results later:
# Asynchronous call returns immediately
async_result = rpyc.async_(calc.add)(5, 3)
# Do other work here...
# Get the result when ready (blocks if not ready)
result = async_result.value
print(f"Result: {result}")
This is useful when calling multiple slow services concurrently.
RPyC Advantages Over XML-RPC
Stateful objects: Remote objects can maintain state across calls.
Richer types: Supports any Python object that can be pickled, including custom classes, nested structures, and functions.
Bidirectional: The server can call methods on client-provided objects (callbacks).
Efficient: Binary pickle format is faster than XML. Pickle is a Python-specific binary serialization format used to convert object hierarchies into a byte stream for storage or transmission
Transparent: Remote objects look and behave like local objects.
RPyC Limitations
Python only: Unlik services like gRPC or Thrift, RPyC only works between Python processes. You cannot call an RPyC service from Java or JavaScript or Go.
Security: Using pickle has security implications. A malicious server could execute arbitrary code on the client.
Network transparency concerns: Making remote calls look exactly like local calls can hide performance costs. A loop that appears to operate on local data might actually make thousands of network calls.
No formal schema: There is no way to describe the service interface formally. Clients must know what methods exist through documentation or code inspection.
Example 3: gRPC
gRPC represents a modern, production-grade approach to RPC. Unlike XML-RPC and RPyC, which use dynamic proxies, gRPC requires you to define your service interface in a schema file and generate stubs before writing any code.
Installation
As with RPyC, gRPC isn’t bundled with Python, so you have to install the framework:
pip install grpcio grpcio-tools --break-system-packages
The Schema
The schema defines the data structures used over the network. This include the definition of the service.
Create calculator.proto:
syntax = "proto3";
package calculator;
// The calculator service definition
service Calculator {
rpc Add (BinaryOperation) returns (Result);
rpc Subtract (BinaryOperation) returns (Result);
rpc Multiply (BinaryOperation) returns (Result);
rpc Divide (BinaryOperation) returns (Result);
}
// A binary operation with two operands
message BinaryOperation {
double a = 1;
double b = 2;
}
// The result of an operation
message Result {
double value = 1;
}
This schema is the contract between client and server. Both sides must agree on this interface. Notice:
-
We define the service (
Calculator) with four methods -
Each method specifies its input type and return type
-
We define message types (
BinaryOperation,Result) that will be sent over the wire -
Field numbers (1, 2) are permanent identifiers; we never change them, and new fields will get different numbers. This allows for schema evolution over time. Those numbers get encoded in the binary serialized representation to identify the specific fields.
Generate Code
Run the Protocol Buffer compiler:
python -m grpc_tools.protoc \
-I. \
--python_out=. \
--grpc_python_out=. \
calculator.proto
This generates two files:
-
calculator_pb2.py: Message class definitions (229 lines) -
calculator_pb2_grpc.py: Service stub and skeleton (40 lines)
These are the static stubs mentioned in the lecture. Unlike XML-RPC and RPyC, which generate proxies at runtime, gRPC generates them ahead of time.
The Server
Create grpc_server.py:
import grpc
from concurrent import futures
import logging
import calculator_pb2
import calculator_pb2_grpc
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger(__name__)
class CalculatorServicer(calculator_pb2_grpc.CalculatorServicer):
"""Implementation of the Calculator service"""
def Add(self, request, context):
"""Add two numbers"""
logger.info(f"Add({request.a}, {request.b})")
result = request.a + request.b
return calculator_pb2.Result(value=result)
def Subtract(self, request, context):
"""Subtract b from a"""
logger.info(f"Subtract({request.a}, {request.b})")
result = request.a - request.b
return calculator_pb2.Result(value=result)
def Multiply(self, request, context):
"""Multiply two numbers"""
logger.info(f"Multiply({request.a}, {request.b})")
result = request.a * request.b
return calculator_pb2.Result(value=result)
def Divide(self, request, context):
"""Divide a by b"""
logger.info(f"Divide({request.a}, {request.b})")
if request.b == 0:
context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
context.set_details("Cannot divide by zero")
return calculator_pb2.Result()
result = request.a / request.b
return calculator_pb2.Result(value=result)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
calculator_pb2_grpc.add_CalculatorServicer_to_server(
CalculatorServicer(), server
)
port = '50051'
server.add_insecure_port(f'[::]:{port}')
logger.info(f"gRPC server listening on port {port}")
try:
server.start()
server.wait_for_termination()
except KeyboardInterrupt:
logger.info("Shutting down server")
server.stop(0)
if __name__ == '__main__':
serve()
Some of the key differences from XML-RPC and RPyC are:
Type safety: The request parameter is a BinaryOperation object with a and b fields. If you try to access a field that doesn’t exist, you get a compile-time error (or at least an IDE warning).
Explicit return types: You must return a Result object. The type system ensures you cannot accidentally return the wrong type.
Error handling: Instead of raising exceptions, you set a status code and details on the context object. This provides structured error reporting.
The Client
Create grpc_client.py:
import grpc
import calculator_pb2
import calculator_pb2_grpc
def main():
# Create a channel to the server
channel = grpc.insecure_channel('localhost:50051')
# Create a stub (client)
stub = calculator_pb2_grpc.CalculatorStub(channel)
print("gRPC Calculator Client")
print("=" * 40)
# Call remote methods
# Note: we must create message objects, not pass raw values
result = stub.Add(calculator_pb2.BinaryOperation(a=5, b=3))
print(f"Add(5, 3) = {result.value}")
result = stub.Subtract(calculator_pb2.BinaryOperation(a=10, b=4))
print(f"Subtract(10, 4) = {result.value}")
result = stub.Multiply(calculator_pb2.BinaryOperation(a=7, b=6))
print(f"Multiply(7, 6) = {result.value}")
result = stub.Divide(calculator_pb2.BinaryOperation(a=15, b=3))
print(f"Divide(15, 3) = {result.value}")
# Error handling
print("\nTesting error handling:")
try:
result = stub.Divide(calculator_pb2.BinaryOperation(a=10, b=0))
print(f"Divide(10, 0) = {result.value}")
except grpc.RpcError as e:
print(f"Error: {e.code()}: {e.details()}")
channel.close()
if __name__ == '__main__':
main()
The key difference here: instead of calling stub.Add(5, 3), we call stub.Add(calculator_pb2.BinaryOperation(a=5, b=3)). We must create the message object explicitly. This is more verbose but provides type safety.
Running the Example
Start the server:
python grpc_server.py
Run the client:
python grpc_client.py
Output:
gRPC Calculator Client
========================================
Add(5, 3) = 8.0
Subtract(10, 4) = 6.0
Multiply(7, 6) = 42.0
Divide(15, 3) = 5.0
Testing error handling:
Error: StatusCode.INVALID_ARGUMENT: Cannot divide by zero
What Is Happening?
The gRPC flow is similar to XML-RPC but with important differences:
-
Schema enforcement: The client cannot call a method that doesn’t exist in the
.protofile. Your IDE will show an error. With XML-RPC, you only find out at runtime. -
Binary serialization: The
BinaryOperationmessage is encoded using Protocol Buffers into a compact binary format, much smaller than XML. -
HTTP/2 transport: The message is sent over HTTP/2, which supports multiplexing (multiple concurrent calls over one connection) and binary framing.
-
Structured errors: Instead of exceptions, gRPC uses status codes (like HTTP status codes but more comprehensive). The client can distinguish between different failure types.
Comparing Code Complexity
Let’s count the code required for each approach (I omitted comments and blank lines):
XML-RPC:
-
Server: 39 lines
-
Client: 22 lines
-
Total: 61 lines, no schema file
RPyC:
-
Server: 59 lines
-
Client: 31 lines
-
Total: 90 lines, no schema file
gRPC:
-
Schema: 15 lines
-
Generated code: 218 lines (automatic, don’t count)
-
Server: 49 lines
-
Client: 25 lines
-
Total: 89 lines user-written code (74 lines of code + the schema)
gRPC requires slightly more code and an extra build step, but provides:
-
Compile-time type checking
-
Better performance (binary encoding)
-
Formal service documentation (the .proto file)
-
Multi-language support (same .proto generates Java, Go, etc.)
gRPC Advantages Over Dynamic RPC
Type safety: Calling a non-existent method or passing the wrong types fails at compile time, not runtime.
Performance: Protocol Buffers are much faster to serialize/deserialize than XML and more compact than pickle.
Schema evolution: You can add new fields to messages without breaking old clients. Protocol Buffers handle forward and backward compatibility.
Language independence: The same .proto file generates code for Python, Java, Go, C++, JavaScript, and many other languages.
Streaming: gRPC supports streaming (not shown in this simple example) for real-time data feeds or large transfers.
Production ready: Built-in support for deadlines, cancellation, load balancing, retries, and health checking.
gRPC Disadvantages
Complexity: Requires a build step to generate code. Can’t just write code and run it like with XML-RPC.
Learning curve: More concepts to learn (Protocol Buffers, HTTP/2, status codes, metadata).
Debugging: Binary wire format is not human-readable. Need specialized tools like grpcurl (but RPyC is binary too)
Browser limitations: Native gRPC doesn’t work in JavaScript in browsers. You need to use gRPC-Web, which has limitations (limited support for streaming and the need for an intermediate proxy).
When to Use Each
XML-RPC:
-
Quick prototypes or educational examples
-
Simple integrations between different languages
-
When human readability of messages is important for debugging
-
Legacy systems that already use XML-RPC
-
Projects where you cannot add dependencies beyond Python’s standard library
RPyC:
-
Python-to-Python communication where you control both ends
-
Remote administration and management tools
-
Distributed Python applications
-
When you need stateful remote objects
-
Internal tools where security concerns are minimal
gRPC:
-
Production microservices
-
High-performance requirements (thousands of requests per second)
-
Polyglot environments (services in different languages)
-
When you need streaming (server push, real-time data)
-
Large-scale distributed systems
-
When compile-time type safety is important
-
Public APIs that need versioning and evolution support
From RPC Concepts to Code
Let’s connect these examples to concepts from the our lecture:
Stub Generation
XML-RPC and RPyC use dynamic stub generation: The client proxy is created at runtime using Python’s dynamic features (__getattr__ in XML-RPC, metaclasses in RPyC). This is convenient (no compilation step) but provides no compile-time type checking.
gRPC uses static stub generation: You write a .proto file and compile it to generate stubs before running your program. This requires an extra build step but catches type errors at compile time.
Trade-off: convenience vs. safety. Dynamic generation is easier to get started with. Static generation catches errors earlier and usually generates more efficient code.
Marshalling and Data Representation
XML-RPC: Uses XML Schema types. Every value is tagged with its type. This format is human-readable but verbose, leading to longer parsing times and more network bandwidth.
RPyC: Uses Python’s pickle, which preserves Python’s object structure, including types, class definitions, and references. It’s efficient but is Python-specific and potentially insecure.
gRPC: Uses Protocol Buffers with explicit schema definitions. It uses a compact binary format with schema evolution support.
Trade-off: readability vs. efficiency vs. language independence.
Handling Failures
All three examples show synchronous RPC where the client blocks waiting for a response. In production code, you may need:
Timeouts: Set a deadline for how long to wait. Without timeouts, a slow server can hang the client indefinitely.
Retries: Automatically retry transient failures. But only for idempotent operations.
Circuit breakers: Track repeated failures and stop calling a failing service to prevent cascading failures.
None of our simple examples implement these.