A virtual actor framework for Python, inspired by Microsoft Orleans
Project description
PyOrleans
PyOrleans is a virtual-actor (“grain”) framework for Python, modeled after the programming model of Microsoft Orleans. It gives you always-addressable actors, turn-based concurrency per grain, pluggable storage with optimistic concurrency, timers, reminders, call filters, and placement hooks—so you can structure distributed-style logic in a single process (or extend toward multiple silos) without manually juggling locks for each entity.
The design goal is to bring the Orleans grain semantics—virtual actors, single-threaded turns, and explicit concurrency attributes—to idiomatic async Python, including the option to exploit free-threaded CPython (no GIL) where available (PEP 703).
Quick start
Install and run a minimal stateful grain in-process:
pip install pyorleans
import asyncio
from dataclasses import dataclass
from pyorleans import Grain, GrainFactory, LocalMemoryStorage, Silo, stateful
@dataclass
class CounterState:
n: int = 0
@stateful(CounterState)
class CounterGrain(Grain):
def add(self, x: int) -> int:
self.state.n += x
return self.state.n
async def main() -> None:
silo = Silo()
silo.register_storage(LocalMemoryStorage())
silo.register_grain_class(CounterGrain)
await silo.start()
factory = GrainFactory(silo)
grain = factory.get_grain(CounterGrain, "my-counter")
total = await grain.add(7)
print(total) # 7
await silo.stop()
if __name__ == "__main__":
asyncio.run(main())
For an HTTP front end (e.g. FastAPI + Granian), see benchmarks/benchmark_apps/pyorleans_api/main.py in this repository.
Architecture (conceptual)
High-level flow for a remote client calling grains through HTTP (as in the benchmark). The C# path uses a real Orleans cluster client and Redis for membership; the Python path hosts Silo inside the same process as the ASGI app.
flowchart TB
subgraph bench["Benchmark client"]
BR["run_benchmark.py\n(aiohttp, POST /benchmark/{id})"]
end
subgraph csharp["Orleans C# stack (Docker)"]
WEB["OrleansPoC.WebAPI\n:8081"]
CLIENT["IClusterClient"]
REDIS[("Redis\nclustering / directory")]
SILO_CS["Orleans silo host\n(orleans-server)"]
G_CS["Stock-style grains"]
WEB --> CLIENT
CLIENT <--> REDIS
SILO_CS <--> REDIS
CLIENT --> G_CS
end
subgraph pyo["PyOrleans stack (Docker)"]
GR["Granian\n(ASGI)"]
API["FastAPI app"]
SILO_PY["PyOrleans Silo\n+ worker threads"]
G_PY["StockBenchmarkGrain"]
GR --> API --> SILO_PY --> G_PY
end
BR --> WEB
BR --> GR
Motivation
- Orleans popularized virtual actors: you address a grain by id and type; the runtime activates it on demand, serializes calls per activation, and manages lifecycle and persistence. See the Orleans documentation and the dotnet/orleans repository for the original model and terminology.
- Python already excels at I/O-bound concurrency with
asyncio. PyOrleans adds a grain boundary: one logical thread of execution per activation, explicit reentrancy and interleaving rules, and state helpers—similar in spirit to Orleans, adapted to Python’s async runtime. - Free-threaded Python (GIL optional/disabled, PEP 703) allows CPU-bound work in multiple native threads for distinct grains; the benchmark contrasts CPython with GIL (3.12) against a free-threaded runtime in Docker (tdciwasaki/python-nogil, Python 3.14) to illustrate throughput under load.
PyOrleans is not a wire-compatible port of Orleans; it is a Python library inspired by the same concepts.
Features
- Grains (
Grain), silo host (Silo), and factory (GrainFactory,GrainReference) - Turn-style execution and concurrency attributes (
reentrant,read_only,stateless_worker, …) - Stateful grains with
StorageProvider,LocalMemoryStorage, and ETag-style conflicts (ETagMismatchError) - Timers and reminders, observers, call filters, placement strategies, in-memory directory
See package exports in src/pyorleans/__init__.py for the full public API.
Installation
pip install pyorleans
Requires Python 3.12+. Optional extras:
pip install pyorleans[redis] # Redis-related integrations when you use them
Development
Clone github.com/antunsz/pyorleans and run tests from the repository root:
pip install -e ".[dev]" # requires a recent pip that supports PEP 660 editable installs
# or, without editable install:
PYTHONPATH=src python -m pytest
Build artifacts locally:
python -m pip install build
python -m build
Benchmark (repository)
The benchmarks/ directory is not part of the published wheel; it is for local experimentation.
C# baseline and inspiration
The C# side follows the same solution shape as the public PoC angelobelchior/OrleansPoC: Application/ with WebAPI, silo server, contracts, Aspire ServiceDefaults, and Redis-backed Orleans clustering. The HTTP workload updates a stock-style grain (name, value, trades, volume), matching the domain used in that PoC and mirrored in the PyOrleans FastAPI app (StockBenchmarkGrain in benchmarks/benchmark_apps/pyorleans_api/main.py). Dockerfile.orleans_server and Dockerfile.orleans_webapi clone that repository at build time (git clone --depth 1); override with build args ORLEANS_POC_REPO and ORLEANS_POC_REF (default main) if you use a fork or pinned branch/tag.
Orleans packages are restored from nuget.org inside the image (benchmarks/docker-nuget.config).
Docker-only patches (under benchmarks/orleans_*_patch/): the PoC normally relies on .NET Aspire (OrleansPoC.AppHost) to call WithClustering(redis) for both silo and client. In Compose we only set ConnectionStrings__redis, so the Dockerfiles overlay Program.cs files that call UseRedisClustering with that connection string. The WebAPI patch also registers GET / and POST /benchmark/{id} for run_benchmark.py. On the silo image, UseDashboard() is omitted: the dashboard would bind HTTP :8080 and collide with Kestrel on the same port, so the silo never finished starting and the client logged “Could not find any gateway”.
Targets compared
| Target | Role |
|---|---|
| Orleans C# | Reference .NET Orleans stack (Orleans overview) |
| PyOrleans (GIL) | CPython with GIL, ASGI served with Granian |
| PyOrleans (NoGIL) | tdciwasaki/python-nogil (3.14-slim-trixie): free-threaded CPython + Granian multi-thread runtime (free-threading, PEP 703) |
The NoGIL service no longer compiles CPython from source; it uses the community image above (based on official Python-style builds with GIL disabled). To confirm in a container: python -c "import sys; print(not sys.flags.gil)" should print True (see the image README on Docker Hub).
Methodology
The runner (benchmarks/run_benchmark.py) drives HTTP POST requests with aiohttp:
| Parameter | Value |
|---|---|
| Concurrency | 200 workers |
| Warmup | 2 s per target |
| Measured window | 10 s per target |
| Multi-grain pool | 100 distinct grain keys (STOCK-0000 …) |
Scenarios
- Single grain — all requests use one id (
BENCH): stresses per-grain serialization and turn throughput (no parallelism across grains). - Multi grain — rotating ids across the pool: stresses many concurrent activations (where free-threading and thread pools can help—subject to host limits and Docker CPU).
Results are environment-specific (CPU, thermal limits, Docker Desktop settings, .NET vs Python builds). Treat them as one sample, not a universal ranking. After changing interpreters (for example moving the NoGIL container to Python 3.14 via tdciwasaki/python-nogil), re-run the benchmark before comparing to older tables.
Example run (sample)
The table below is a single local run after make benchmark (all three services healthy). Re-run on your machine for comparable numbers.
Scenario 1 — single grain (fully serialized on one activation)
| Target | RPS | Avg ms | p50 ms | p95 ms | p99 ms |
|---|---|---|---|---|---|
| Orleans C# [1 grain] | 17,943.7 | 11.1 | 11.0 | 12.0 | 15.0 |
| PyOrleans (GIL) [1 grain] | 10,707.1 | 18.7 | 15.9 | 31.9 | 46.8 |
| PyOrleans (NoGIL) [1 grain] | 7,190.7 | 27.8 | 27.7 | 29.2 | 39.6 |
Scenario 2 — multi grain (100 distinct grains)
| Target | RPS | Avg ms | p50 ms | p95 ms | p99 ms |
|---|---|---|---|---|---|
| Orleans C# [100 grains] | 17,749.9 | 11.3 | 11.1 | 12.2 | 15.2 |
| PyOrleans (GIL) [100 grains] | 11,805.1 | 16.9 | 15.7 | 23.8 | 26.5 |
| PyOrleans (NoGIL) [100 grains] | 7,357.2 | 27.2 | 26.9 | 28.8 | 45.0 |
In this sample, Orleans C# had the highest RPS in both scenarios; PyOrleans (GIL) improved from single- to multi-grain (less head-of-line blocking across keys). PyOrleans (NoGIL) was slower here—often consistent with extra threading / allocator / build overhead in a small, allocation-heavy HTTP+grain path; interpret with profiling on your target hardware.
How to run
From the repo root (Docker Compose v2):
make up # builds and starts services in benchmarks/docker-compose.yml
make benchmark # runs the client under benchmarks/, then tears down
Or manually:
docker compose -f benchmarks/docker-compose.yml up -d --build
cd benchmarks && pip install aiohttp && python run_benchmark.py
References
- Microsoft Orleans docs, dotnet/orleans
- angelobelchior/OrleansPoC — PoC layout and stock-style grain scenario that the C# benchmark is aligned with
- PEP 703, Free-threading HOWTO
- Granian
- tdciwasaki/python-nogil (benchmark
Dockerfile.pyorleans.nogilbase image)
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyorleans-0.2.0.tar.gz.
File metadata
- Download URL: pyorleans-0.2.0.tar.gz
- Upload date:
- Size: 29.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7b208fb250db001de750fb8b1eb751be3040073a5044a460cbd53a3a2689ef4d
|
|
| MD5 |
d0e40e4aa40709f679ad718c66d9a40b
|
|
| BLAKE2b-256 |
3fa2d1847b3c0f6ce09f50b83a70b5f36b7ec309d03e96b53ca221dc5abf3209
|
File details
Details for the file pyorleans-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pyorleans-0.2.0-py3-none-any.whl
- Upload date:
- Size: 35.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
319a2055d2e9a3a8f7522ea8da3b50a98bad245590ea206c085f5ce5efd821f6
|
|
| MD5 |
56f7cf9d812f568ac913de6c3cd5c6a0
|
|
| BLAKE2b-256 |
ab29a1602503fa7b9af44012496e89aaac126c4590a52b05a0bc12907cb2a1dd
|