A simple, modern, type-safe actor framework for asynchronous, fault-tolerant systems.
tractor gives you isolated, single-threaded actors that communicate only by
message passing on top of asyncio. Messages are ordinary typed objects, so
your editor and type checker understand exactly which actor a message targets
and what it replies with — no Any, no string dispatch.
- Type-safe messaging —
Message[A, R]ties a message to the actor typeAit's sent to and the reply typeRit produces.askreturnsR;tellreturns nothing. Mismatches are caught statically. ask/tellsemantics — request/reply or fire-and-forget, plus non-blockingtry_ask/try_telland reply forwarding for delegation.- Lifecycle hooks —
on_start,on_stop, andon_panicwith a per-actorControlFlowdecision (stop or keep running). - Fault tolerance — panics are isolated to the actor and reported to a
pluggable
CrashPolicy. - Custom work sources — override
stepto await timers, futures, or streams alongside the inbox using the biasedselect. - Fully typed — ships
py.typed; built around PEP 695 generics.
- Python 3.14+ (the API uses recent typing features).
pip install tractor
# or
uv add tractorimport asyncio
from dataclasses import dataclass
from typing import override
from tractor import Actor, Message, Runtime
from tractor.message import Context
class Counter(Actor):
def __init__(self) -> None:
self.count = 0
@dataclass
class Increment(Message[Counter, None]):
by: int = 1
@override
async def dispatch(self, actor: Counter, ctx: Context[Counter]) -> None:
actor.count += self.by
@dataclass
class Get(Message[Counter, int]):
@override
async def dispatch(self, actor: Counter, ctx: Context[Counter]) -> int:
return actor.count
async def main() -> None:
runtime = Runtime()
counter = runtime.spawn(Counter())
await runtime.tell(counter, Increment(by=3)) # fire-and-forget
await runtime.tell(counter, Increment())
total = await runtime.ask(counter, Get()) # request/reply -> int
print(total) # 4
await counter.stop()
if __name__ == "__main__":
asyncio.run(main())Actor— your state lives here. Subclass it and add plain methods; lifecycle hooks (on_start/on_stop/on_panic) andstepare all optional with sensible defaults.Message[A, R]— a typed, dispatchable message. Implementdispatchto invoke the actor and produce the replyR.Runtime— created once at startup;spawns actors and routesask/tell. Inside a handler, usectx.ask/ctx.tellso sender identity is carried through.ActorRef[A]— an opaque handle to a spawned actor; the only way to address it.CrashPolicy— observer invoked after every panic (defaults to logging).
See examples/ for a runnable pub/sub demo covering fanout,
lifecycle hooks, and a custom step heartbeat:
python examples/pub_sub.pyThis repo uses uv and provides a Nix flake.
# with uv
uv sync
uv run pytest
# with Nix (provides Python 3.14 + uv, tractor installed editable)
nix develop
pytestBuild the PyPI artifacts (sdist + wheel) reproducibly with Nix:
nix build # -> ./result/{tractor-*.tar.gz, tractor-*.whl}MIT © Shane Murphy