ohra
Apache Pekko Typed actors on asyncio, for Python 3.12+. No dependencies. Ohra is Finnish for barley, the crop of Pekko, the Finnish god of barley and brewing.
ohra is an independent project, built on the design of Apache Pekko and, before it, Akka, with thanks to both communities. It follows their semantics and names closely, but it is not part of the Apache Software Foundation.
An actor is a function from a message to the next behavior. Typed refs make wrong messages a type error under pyright strict. Everything runs on one event loop, so a handler can await without losing the guarantee that an actor handles one message at a time. numpy work goes to threads and comes back as a message. ohra.testkit, Pekko's test kit, tests actors with probes and fires timers on a manual clock.
import asyncio
from dataclasses import dataclass
from ohra import ActorRef, ActorSystem, Behavior, Behaviors
@dataclass(frozen=True)
class Increment:
by: int
@dataclass(frozen=True)
class GetTotal:
reply_to: ActorRef[int]
type Command = Increment | GetTotal
def counter(total: int = 0) -> Behavior[Command]:
@Behaviors.receive_message
def on_message(msg: Command) -> Behavior[Command]:
match msg:
case Increment(by):
return counter(total + by) # state is the next behavior
case GetTotal(reply_to):
reply_to.tell(total)
return Behaviors.same
return on_message
async def main() -> None:
async with ActorSystem(counter(), "demo") as system:
system.tell(Increment(5))
print(await system.ask(GetTotal, timeout=1)) # 5
if __name__ == "__main__":
asyncio.run(main())
If you know Apache Pekko or Akka Typed, the names are the same in snake_case, and the semantics are checked against the Apache Pekko source. If you don't, the book explains everything from the first actor to a sharded web app on Kubernetes.
Install
pip install ohra # the library
pip install 'ohra[numpy]' # plus numpy, for ohra.ext.shm and the examples that use it
pip install 'ohra[web]' # plus FastAPI and uvicorn, for ohra.ext.fastapi and the fastapi_* examples
pip install 'ohra[postgres]' # plus psycopg, for ohra.ext.postgres, the durable state store for clusters
pip install 'ohra[torch]' # plus PyTorch, for the purchase predictor example
pip install 'ohra[numba]' # plus numba, for compiled loops in threads and examples/stockouts.py
pip install 'ohra[testkit]' # plus pytest and pytest-asyncio, for ohra.testkit and its pytest fixtures
The examples and tests live in the repository, not the package. To run them, clone it and install it in editable mode, with the dev extra for pytest, pytest-asyncio, pyright, httpx and ruff:
git clone https://github.com/hanishi/ohra.git && cd ohra
pip install -e '.[dev,numpy,web,postgres,torch,numba,testkit]' # or: uv sync --all-extras
Testing
ohra.testkit is Apache Pekko's actor test kit: a test probe stands in for the actor on the other end, and manual time fires timers without waiting for them. Install 'ohra[testkit]', then turn on the pytest plugin in conftest.py and pytest-asyncio's auto mode in pyproject.toml:
# conftest.py
pytest_plugins = ["ohra.testkit.pytest_plugin"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
A test for the counter above, saved as counter.py:
from counter import GetTotal, Increment, counter
from ohra.testkit import ActorTestKit, TestProbe
async def test_counter_adds_up(testkit: ActorTestKit):
ref = testkit.spawn(counter())
probe: TestProbe[int] = testkit.create_test_probe()
ref.tell(Increment(2))
ref.tell(Increment(3))
ref.tell(GetTotal(probe.ref))
await probe.expect_message(5)
The testkit fixture gives each test its own actor system and shuts it down afterwards. probe.ref goes where the actor replies, and expect_message waits up to 3 seconds for the next message, failing with expected 5, found 4 or timeout (3s) during expect_message while waiting for 5. Mark a test @pytest.mark.manual_time and its manual_time fixture moves the clock: await manual_time.time_passes(3600) fires an hour-long timer at once. Chapter 12 has the full guide and recipes for DeathWatch, restarts, backoff, persistence and sharding; the tests in examples/testing/ run with pytest examples/testing.
The book
How ohra works is the guide and the reference, one chapter per layer, with diagrams:
- Actors: behaviors, ask, children and DeathWatch, supervision, timers and stash, routers, the runtime model
- The actor system: guardians, extensions, the receptionist, coordinated shutdown
- Persistence: write, then install, then reply; recovery in one read
- Remoting: addresses,
@remotablemessages, connections - Cluster membership: gossip, the member lifecycle, failure detection, the split-brain resolver
- Cluster singleton: one actor on the oldest member, and the hand-over
- Sharding: regions, shards and the coordinator, in one process and across nodes
- Management and Kubernetes: health checks, bootstrap, discovery, running on Docker Desktop
- FastAPI: the actor system owned by the app, routes that ask actors
- numpy and shared memory: blocking work in a thread, loops compiled with numba, large arrays to another process without a copy
- Putting it together: one cart request through every layer
- Testing: the test kit, probes, manual time for timers, pytest fixtures
Appendices: Apache Pekko names, known deviations from Apache Pekko, performance, and a glossary.
Examples
| File | Shows |
|---|---|
examples/counter.py |
the functional style, ask, supervision with resume |
examples/chatroom.py |
the ChatRoom from the Apache Pekko docs: sessions as children, narrowed refs, DeathWatch |
examples/remoting/ |
two processes: greeter_node.py listens on 2552, client_node.py resolves it by path, tells and asks across the wire |
examples/cluster/ |
node.py NAME PORT [SEED_PORT ...], or node.py NAME PORT --bootstrap PORT... to form the cluster with no seeds through Pekko's bootstrap: start three in three terminals, watch them join, go Up and elect a leader, each node's greeter appear in every node's Receptionist listing, one counter singleton answer all of them, and the sharded counters for three ids spread over the nodes; ctrl-c a node, which leaves gracefully, and the singleton and the shards it hosted move to the others; management on port + 1000 (curl localhost:3551/cluster/members) |
examples/k8s/ |
node.py, Dockerfile and cluster.yaml: a three-pod cluster on Docker Desktop Kubernetes that finds itself through the API server and spreads a simulated calculation over the pods with sharding; delete a pod and its tasks move, scale up and the new pods take shards; see Running on Docker Desktop Kubernetes |
examples/k8s/carts.py |
the FastAPI carts on a three-pod cluster: any pod serves any cart, each cart lives on one pod, persisted in Postgres, and moves with its items when that pod goes; carts.yaml, see Carts |
examples/persisted_counter.py |
Behaviors.persisted with SqliteStore: the total survives a supervised restart and a new actor system; run it twice and it keeps counting |
examples/database_client.py |
backoff restarts of a failing setup, a stash that outlives restarts, timers, ctx.ask |
examples/interaction_patterns/ |
one runnable file per pattern in Apache Pekko's Interaction Patterns page: fire and forget, request-response, adapted response, ask between actors, ask from outside, generic response wrapper, ignoring replies, pipe to self, per-session child, aggregator, tail chopping, scheduling to self |
examples/fastapi_carts.py |
a FastAPI service over actors: one entity per cart id through Sharding, persisted in SQLite with Behaviors.persisted so carts survive passivation and restarts, typed ask from routes to entities |
examples/fastapi_chatroom.py |
the chat room over WebSockets: actors.websocket owns each socket and gives it a connection actor, as Play's ActorFlow.actorRef does, rooms created on demand, and DeathWatch turning a closed socket into a "left" notice; serves a browser page at / |
examples/fastapi_online_learning.py |
a feedback loop behind FastAPI: predictions micro-batched into one numpy call, outcomes posted back, a learner actor taking gradient steps in a thread and hot-swapping the model; serves a playground page at /, and python examples/fastapi_online_learning.py runs the server with a demo client |
examples/fastapi_purchase_predictor.py |
the same loop with PyTorch: a model of which shopper buys which product, learned from simulated shoppers with hidden tastes; the learner trains its own copy in a thread and sends the predictor copies of the weights; the page at / shows the true tastes beside the model's predictions, accuracy over time, and a button that shifts the tastes so you can watch the model catch up. Needs the web and torch extras |
examples/bandit.py |
Thompson sampling over sharded arms: Beta posteriors as arrays, one vectorized draw per shard, ctx.ask fan-out, a progress timer |
examples/benchmark.py |
message throughput, round-trip latency and spawn cost of the runtime; the source of the Performance table |
examples/compute.py |
numpy in threads via pipe_to_self(asyncio.to_thread(...)), with a latency probe |
examples/batching.py |
batching throughput and p99 latency curves |
examples/population.py |
100k single-arm actors against 16 shards behind Routers.pool |
examples/shared_memory.py |
SharedArray against pickling for a 400MB array |
examples/stockouts.py |
a loop numpy cannot vectorize, a stockout simulation, four ways: pure Python in a thread, in a process with shared memory, numba in a thread, and numba with nogil=True in a thread, one analyst and eight at once, with the event loop's lag. Needs the numba extra: uv run --extra numba python examples/stockouts.py |
bandit, compute, batching, population and shared_memory need numpy: uv run --extra numpy python examples/bandit.py. The fastapi_* examples need the web extra and run under uvicorn: uv run --extra web uvicorn examples.fastapi_carts:app --port 8000.
Development
Layout follows Apache Pekko's modules: ohra is the actor core, ohra.remote the transport and serialization, ohra.cluster membership and the singleton, ohra.cluster.sharding the Shard, region and coordinator, ohra.ext the FastAPI, Postgres and shared-memory integrations, ohra.testkit the test kit. Every public name is importable from ohra and from its own subpackage, except the test kit's, which come from ohra.testkit only.
pytest # every test fails if any asyncio task outlives it
pyright # strict; tests/typing_check.py asserts type errors that must occur
See CONTRIBUTING.md for setup, the runtime invariants and how to port from Apache Pekko. Report vulnerabilities as described in SECURITY.md.
License
Apache-2.0, the same as Apache Pekko. The chat room and interaction-pattern examples are derived from the Apache Pekko documentation; see NOTICE.
ohra is an independent project, not affiliated with or endorsed by the Apache Software Foundation or the Apache Pekko project. Apache Pekko is a trademark of the Apache Software Foundation.
Metadata
Release files for ohra 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ohra-0.4.0.tar.gz | 176.9 kB | With provenance | Details for ohra-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ohra-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for ohra-0.4.0-py3-none-any.whl |
Total release size: 332.2 kB
Release files / ohra-0.4.0.tar.gz
| Download URL | ohra-0.4.0.tar.gz |
|---|---|
| Size | 176.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ca272282576dfe3a4572e2ac5896951b63a4c572e7762b73d637fb6a3a2d3efd
|
|
BLAKE2b-256 checksum How to use checksums |
0342e7bd26f90a5edca8acd479e6e0f8e95fb332514c0894778999c98cbc70d9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logRelease files / ohra-0.4.0-py3-none-any.whl
| Download URL | ohra-0.4.0-py3-none-any.whl |
|---|---|
| Size | 155.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
05a058d1ba5a98efc9248eb933055bc15d641bee39ab7414ee210d29f4a50746
|
|
BLAKE2b-256 checksum How to use checksums |
c2d3d9dfaf5579e11d07eede75246e8aa8081e422771e5a338c01c7296b3d376
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logSecurity for ohra 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ohra-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.