Erlang/OTP primitives for Python.
Project description
fauxtp
Erlang/OTP primitives for Python.
It brings GenServer, Supervisor, and pattern-matched message passing to Python's async ecosystem (via anyio).
It is not fast. It is not the BEAM. It is, however, a way to write concurrent Python that doesn't make you want to quit programming.
Creation
Most of this project was initially vibecoded. However at this point I've had to put my grubby little mitts on it enough that I'm pretty confident that what it's doing is correct, and if it isn't then it's my fault too.
Install
uv add fauxtp
The Gist
Note: actors are started inside an AnyIO TaskGroup (structured concurrency). You must pass
task_group=...topython.Actor.start().
GenServer
If you know OTP, you know this. If you don't: it's a stateful actor that handles synchronous calls and asynchronous casts.
from fauxtp import GenServer, call, cast
import anyio
class Counter(GenServer):
async def init(self):
return {"count": 0}
async def handle_call(self, request, _from, state):
match request:
case "get":
return state["count"], state
case ("add", n):
new_count = state["count"] + n
return new_count, {"count": new_count}
async def handle_cast(self, request, state):
match request:
case "reset":
return {"count": 0}
async def main():
async with anyio.create_task_group() as tg:
pid = await Counter.start(task_group=tg)
print(await call(pid, ("add", 5))) # 5
await cast(pid, "reset")
print(await call(pid, "get")) # 0
anyio.run(main)
Supervisors
Let it crash. The supervisor restarts it.
import anyio
from fauxtp import GenServer, call
from fauxtp.registry import Registry
from fauxtp.supervisor import Supervisor, ChildSpec, RestartStrategy
class Counter(GenServer):
async def init(self):
return {"count": 0}
async def handle_call(self, request, _from, state):
match request:
case "get":
return state["count"], state
async def main():
async with anyio.create_task_group() as tg:
registry = await Registry.start(task_group=tg)
_sup_pid = await Supervisor.start(
children=[
ChildSpec(actor=Counter, name="c1"),
ChildSpec(actor=Counter, name="c2"),
],
strategy=RestartStrategy.ONE_FOR_ONE,
registry=registry,
task_group=tg,
)
c1 = await call(registry, ("get", "c1"))
print(c1) # PID(...) or None
anyio.run(main)
What's inside?
- Actors:
send,receive(with pattern matching). - GenServer:
call,cast,info. - Supervisors:
one_for_one,one_for_all. - Registry:
register(name, pid),whereis(name).
Why?
Async Python often devolves into a mess of unmanaged tasks and race conditions. OTP solved this decades ago with structured concurrency trees. We're just borrowing their homework.
Recommendations for more Elixir-like Python
The fauxtp code only requires anyio as a dependency, and that will remain true. However, if you want to have even more Elixir shenanigans, we can make the following recommendations:
-
Treat server state as a value.
- Do not mutate the incoming state in-place, always return a new state.
- While this isn't required (Python is pass by reference, so any mutated state should apply to the overall state), we make no guarantees as to whether or not things will break.
-
Prefer forcibly immutable/persistent state containers.
- Avoid Python stdlib mutable collections (
list,dict,set, mostcollections.*), except for tuples (and other immutables). - Consider
rpds-pyorpyrsistentfor persistent vectors/maps/sets.
- Avoid Python stdlib mutable collections (
-
Avoid using
typing.Anyin user code where possible.- While we have to use it internally to avoid exploding errors (and some of this may leak to the more publicly aimed APIs), we highly recommend using
Generic[T]where needed, as ourGenServerdoes to specify its request and state types.
- While we have to use it internally to avoid exploding errors (and some of this may leak to the more publicly aimed APIs), we highly recommend using
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 fauxtp-0.3.1.tar.gz.
File metadata
- Download URL: fauxtp-0.3.1.tar.gz
- Upload date:
- Size: 126.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"openSUSE Tumbleweed","version":"20260123","id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
82198ea5a937ba019d688587e0595f1dceb703aaf3c45f1a20483d0c33fefa6a
|
|
| MD5 |
8700b84ec05d319580ed74b331c0bb27
|
|
| BLAKE2b-256 |
aced82f947690a75dee2b2db9a4773a4540281cc87233be490b4bd0322f31f36
|
File details
Details for the file fauxtp-0.3.1-py3-none-any.whl.
File metadata
- Download URL: fauxtp-0.3.1-py3-none-any.whl
- Upload date:
- Size: 20.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"openSUSE Tumbleweed","version":"20260123","id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a0a8185b0e7ae95fccf5b7c721d0a404567c9c1d05b62f95bc4651ba962bdec7
|
|
| MD5 |
046c77eea0247ec7cb9ec961c18f09d1
|
|
| BLAKE2b-256 |
46ab553c791a5d32d3945bfd53d31607a7777042d8eb5413eac5f4a6256c976c
|