SimpleRPyC (pronounced simple-are-pie-see), short for Simple Remote Python Call, is a WebSocket-based RPC library for Python. Inspired by RPyC, it uses transparent proxy objects to let you interact with remote Python objects as if they were local—leveraging Python's dynamic nature to bridge processes and machines seamlessly. Additionally, SimpleRPyC provides convenient module patching that allows you to use existing code with minimal changes.
Unlike traditional RPC libraries that use custom protocols, SimpleRPyC is built on WebSocket (standard, debuggable protocol) and msgpack (efficient binary serialization). This choice enables seamless numpy array transfers between different numpy versions (1.x ↔ 2.x), which is critical for running modern ML code alongside legacy environments.
Problem: Running code with incompatible dependencies in the same process.
For example, robotics simulation environments like SimplerEnv (a robot manipulation benchmark) require numpy<2.0, but modern RL policies require numpy>=2.0. You can't install both in the same environment.
Solution: Run them in separate processes and communicate via RPC.
| Feature | SimpleRPyC | RPyC | Pyro5 | zero |
|---|---|---|---|---|
| Transport | WebSocket | TCP + custom protocol | TCP + custom protocol | ZMQ |
| Serialization | msgpack | brine (custom) | serpent | msgspec |
| NumPy support | ✅ Built-in | ✅ Yes | ❌ No | ❌ No |
| NumPy 1.x ↔ 2.x | ✅ Yes | ❌ No | ❌ No | ❌ No |
| Multiple args | ✅ f(a,b,c) |
✅ Yes | ✅ Yes | ❌ No |
| Module patching | ✅ Yes | ✅ Yes | ❌ No | ❌ No |
| Proxy objects | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No |
RPyC: Uses TCP with custom framing/channel layer and "brine" serialization. Supports numpy arrays but breaks when transferring between numpy 1.x and 2.x due to pickle format changes. Custom serialization format makes it hard to add additional features.
Pyro5: Uses TCP with custom wire protocol and serpent serializer. No built-in numpy support - requires manual workarounds that don't handle numpy 1.x ↔ 2.x transfers.
zero: Promising msgspec+ZMQ stack, but doesn't support multiple arguments (remote_fn(a, b, c) fails) or proxy objects.
- WebSocket Transport: Standard protocol, easy to debug, works through firewalls
- msgpack + NumPy: Efficient binary serialization with cross-version numpy compatibility via msgpack-numpy
- Transparent Proxies: Remote objects behave like local objects
- Explicit Materialization: Control when to transfer data from server (
materialize()) - Flexible API: Module patching (
import os) or explicit access (conn.modules.os) - Function Teleportation: Send local functions to remote server with
conn.teleport()using dill - Remote Execution: Run arbitrary code on server with
conn.eval()andconn.execute()
SimpleRPyC uses unencrypted WebSocket connections (ws://). The authentication token and all data are transmitted in plaintext, making them vulnerable to network sniffing and man-in-the-middle attacks.
Use SimpleRPyC only in trusted, private networks (e.g., localhost, private LANs, VPNs). DO NOT expose SimpleRPyC servers to the public internet or use it over untrusted networks.
For production use over untrusted networks, consider:
- Running SimpleRPyC only on
localhostand using SSH tunneling for remote access - Deploying within a private network or VPN
- Adding TLS/SSL encryption layer (e.g., reverse proxy with nginx/caddy)
pip install simplerpycpython -m simplerpyc.serverThe server will print a token. Set it as environment variable:
export SIMPLERPYC_TOKEN='<TOKEN_FROM_SERVER>'SimpleRPyC provides two API styles - choose what fits your use case:
import simplerpyc
from simplerpyc import materialize
# Connect to server
conn = simplerpyc.connect("localhost", 8000)
# Patch modules to use remote versions
simplerpyc.patch_module(conn, "os")
simplerpyc.patch_module(conn, "numpy")
# Import and use as if they were local
import os
import numpy as np
cwd = materialize(os.getcwd())
arr = materialize(np.array([1, 2, 3]))
conn.disconnect()from simplerpyc import connect, materialize
# Connect to server
conn = connect("localhost", 8000)
# Access remote modules explicitly
remote_os = conn.modules.os
remote_np = conn.modules.numpy
# Everything returns proxies by default
cwd = materialize(remote_os.getcwd())
arr = materialize(remote_np.array([1, 2, 3]))
conn.disconnect()See example_client.py for comprehensive examples including:
- Understanding proxies and materialization
- NumPy array operations (1D and 2D)
- Remote code execution (
evalandexecute) - Nested data structures with NumPy arrays
All remote operations return RPCProxy objects that behave like the actual objects:
# Remote function call returns proxy
arr_proxy = remote_np.array([1, 2, 3]) # RPCProxy, not actual array
# Proxy supports attribute access, method calls, indexing
mean_proxy = arr_proxy.mean() # RPCProxy
item_proxy = arr_proxy[0] # RPCProxy
# Chain operations without network round-trips
result_proxy = remote_np.array([1, 2, 3]).reshape(3, 1).mean() # Still proxyUse materialize() to transfer data from server to client:
# Materialize when you need actual values
arr = materialize(arr_proxy) # numpy array
mean = materialize(mean_proxy) # float
item = materialize(item_proxy) # int
# Materialize complex structures
env = simpler_env.make('...') # RPCProxy
obs, reward, done, truncated, info = materialize(env.step(action)) # actual valuesDesign principle: Minimize network transfers by keeping operations on server until results are needed.
from simplerpyc import connect
conn = connect(host="localhost", port=8000, token=None)
# Token auto-detected from SIMPLERPYC_TOKEN env var if not providedModule Patching:
import simplerpyc
simplerpyc.patch_module(conn, "os") # Patch sys.modules
import os # Now uses remote versionNamespace Access:
remote_os = conn.modules.os # Access remote module
remote_len = conn.builtins.len # Access remote builtinUtility Functions:
from simplerpyc import materialize, is_proxy
value = materialize(proxy) # Convert proxy to actual value
is_remote = is_proxy(obj) # Check if object is a proxyRemote Code Execution:
# Evaluate expression
result = materialize(conn.eval("2 + 3")) # 5
# Execute code (no return value)
conn.execute("x = 42")
x = materialize(conn.eval("x")) # 42Function Teleportation:
# Send local function to remote
def square(x):
return x ** 2
remote_square = conn.teleport(square)
result = materialize(remote_square(5)) # 25Connection Management:
conn.disconnect() # Close connectionpython -m simplerpyc.server [--host HOST] [--port PORT]The server will print a token. Set it as environment variable:
export SIMPLERPYC_TOKEN='<TOKEN_FROM_SERVER>'- Standard protocol: Works with existing infrastructure (proxies, load balancers)
- Bidirectional: Server can push data to client (future feature)
- Debuggable: Easy to inspect with browser dev tools or Wireshark
- Firewall-friendly: Uses standard HTTP ports
Why msgpack + msgpack-numpy?
- Efficient: Binary format, smaller than JSON
- NumPy support: Native serialization for numpy arrays
- Cross-version: Works between numpy 1.x and 2.x (unlike pickle)
- Language-agnostic: Can implement clients in other languages
Inspired by RPyC's elegant proxy pattern:
- Transparent: Remote objects behave like local objects
- Efficient: Chain operations on server without network round-trips
- Explicit control: User decides when to transfer data with
materialize()
Unlike RPyC, we use WebSocket transport and msgpack serialization for better numpy compatibility.
simplerpyc/
├── client/
│ ├── connection.py # WebSocket connection management
│ ├── proxy.py # Minimal RPCProxy implementation
│ └── patcher.py # sys.modules patching
├── server/
│ ├── server.py # WebSocket server
│ └── executor.py # Per-client execution context
└── common/
└── serialization.py # msgpack with numpy support
Python 3.10+ is required. Python 3.9 is not supported due to:
- WebSocket concurrency issues with nested asyncio event loops in synchronous RPC context
nest_asynciocompatibility issues causingConcurrencyErrorin websockets
Dependencies (automatically installed with pip install simplerpyc):
websockets>=12.0- WebSocket transport layer (v12.0+ for improved async support)msgpack>=1.0.0- Efficient binary serializationmsgpack-numpy>=0.4.8- NumPy array serialization with cross-version compatibility (1.x ↔ 2.x)dill>=0.3.0- Function serialization forconn.teleport()
MIT