doeff - Algebraic Effects for Python
doeff is an algebraic effects runtime for Python with one-shot continuations and a Rust VM.
Programs are written with generator-based @do notation and executed through explicit handler stacks.
Installation
pip install doeff
Note on
doeff-agentsfrom PyPI:doeff-agents0.1.1 on PyPI was built for doeff 0.4 and does not work with doeff 0.5 — importing its handlers fails withImportError: cannot import name 'WithHandler' from 'doeff'. To keep using that release, pin the doeff 0.4 line:pip install doeff-agents "doeff<0.5". Thedoeff-agentsin this repository (packages/doeff-agents) is not published to PyPI.
Quick Start
from doeff import do, run
from doeff_core_effects import Get, Put, Tell
from doeff_core_effects.handlers import reader, state, writer
from doeff_core_effects.scheduler import scheduled
@do
def counter_program():
yield Put("counter", 0)
yield Tell("Starting computation")
count = yield Get("counter")
yield Put("counter", count + 1)
return count + 1
# Compose handlers explicitly by calling each handler installer.
prog = counter_program()
prog = writer(prog)
prog = state()(prog)
prog = reader(env={"greeting": "hello"})(prog)
result = run(scheduled(prog))
print(result) # 1
Runtime API
| Entrypoint | Signature | Use case |
|---|---|---|
run |
run(doexpr) |
Execute a DoExpr program to completion |
run() takes a single DoExpr (program node) and returns the result value directly.
Handlers are composed explicitly by calling a Program -> Program handler installer.
Use scheduled(prog) to wrap with the scheduler for concurrency effects.
Handler Composition
Handlers are installed by direct call. Stack multiple handlers by wrapping the program:
from doeff import do, run
from doeff_core_effects import Ask
from doeff_core_effects.handlers import reader
@do
def prog():
return (yield Ask("greeting"))
result = run(reader(env={"greeting": "hello"})(prog()))
print(result) # hello
Reusable custom handlers should expose the same Program -> Program shape. In Hy, defhandler
creates that shape directly:
(require doeff-hy.handle [defhandler])
(require doeff-hy.macros [do! <-])
(import doeff [Ask run])
(defhandler ask-env
(Ask [key]
(resume (get {"greeting" "hello"} key))))
(setv greet
(do!
(<- greeting (Ask "greeting"))
greeting))
(print (run (ask-env greet))) ; hello
Effect Surface
Core effects (from doeff_core_effects):
- Reader:
Ask(key),Local(env, program) - State:
Get(key),Put(key, value) - Writer:
Tell(message),slog(msg, **kwargs) - Error:
Try(program)— returnsOk(value)orErr(error) - Observe:
Listen(program, types=...)— collect effects during execution
Scheduler effects (from doeff_core_effects.scheduler):
- Concurrency:
Spawn(program),Wait(task),Gather(*tasks),Race(*tasks) - Promises:
CreatePromise(),CompletePromise(p, v),FailPromise(p, e) - External:
CreateExternalPromise()— bridge external threads - Semaphores:
CreateSemaphore(n),AcquireSemaphore(s),ReleaseSemaphore(s)
Handlers
Built-in handlers (from doeff_core_effects.handlers):
| Handler | Installer | Effects handled |
|---|---|---|
| Reader | reader(env={...}) |
Ask |
| Lazy Ask | lazy_ask(env={...}) |
Ask, Local (with caching) |
| State | state(initial={...}) |
Get, Put |
| Writer | writer |
Tell / WriterTellEffect |
| Try | try_handler |
Try |
| Slog | slog_handler |
Slog |
| Local | local_handler |
Local |
| Listen | listen_handler |
Listen |
| Await | await_handler() |
Await |
| Scheduler | scheduled(prog) |
Spawn, Wait, Gather, Race, etc. |
writer keeps its log in state: install state() outside it, and read the log with
yield writer_log() (from doeff_core_effects.handlers).
Scheduler and Concurrency
from doeff import do, run
from doeff_core_effects import Tell
from doeff_core_effects.handlers import state, writer
from doeff_core_effects.scheduler import scheduled, Spawn, Gather
@do
def worker(label):
yield Tell(f"working: {label}")
return label
@do
def main():
t1 = yield Spawn(worker("a"))
t2 = yield Spawn(worker("b"))
results = yield Gather(t1, t2)
return results
# writer keeps its log in state, so install state() outside writer.
result = run(scheduled(state()(writer(main()))))
print(result) # ['a', 'b']
CLI
# Run a program with auto-discovered interpreter
doeff run --program myapp.module.program
# Inline Python code
doeff run -c 'return 42'
# Inline Hy code (preferred — compose handlers inline)
doeff run --hy '(import myapp [p]) (import doeff-core-effects [lazy-ask]) ((lazy-ask :env myapp.env_dict) p)'
# JSON output
doeff run --program myapp.program --format json
Note: Legacy flags
--interpreter,--env,--set,--apply, and--transformstill work but emit deprecation warnings. Use--hyfor inline handler composition instead.
The CLI supports automatic interpreter/environment discovery via doeff-indexer.
See docs/14-cli-auto-discovery.md for marker syntax and hierarchy rules.
The agent session host that connects to an agent control plane (doeff-sessionhost)
and the doeff-agents CLI are documented in
packages/doeff-agents/README.md.
Performance Benchmarks
Rust VM dispatch micro-benchmarks live in packages/doeff-vm-core/benches/ and
use Criterion. Run them from the VM core crate with the VM feature enabled:
cd packages/doeff-vm-core
cargo bench --features python_bridge
If PyO3 selects the wrong Python installation on macOS, pin it to the uv environment:
cd packages/doeff-vm-core
PYO3_PYTHON="$(cd ../.. && uv run python -c 'import sys; print(sys.executable)')" \
cargo bench --features python_bridge
Python end-to-end benchmarks live in benchmarks/benchmark_runner.py. A normal
run writes JSON to benchmarks/results/<yyyymmdd>-<hostname>.json:
uv run python benchmarks/benchmark_runner.py --runs 20
Compare a fresh run against a committed baseline with:
uv run python benchmarks/benchmark_runner.py \
--compare benchmarks/results/<baseline>.json
CI uses smoke mode only. It runs one iteration with the smallest N values and checks that the benchmark entrypoints still execute; it does not gate on performance:
make bench-smoke
Development
make sync # install deps (Rust VM is rebuilt only when its sources change)
uv run pytest # run full test suite
Warning:
uv syncalone does NOT rebuild the Rust VM extension. Always usemake syncafter editing.rsfiles underpackages/doeff-vm/.
Maintainers: the landing queue and the verification rigs live in
docs/maintainers.md.
License
MIT License. See LICENSE.
Metadata
Release files for doeff 0.5.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 | |
|---|---|---|---|---|
| doeff-0.5.0.tar.gz | 60.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| doeff-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 112.8 kB
Release files / doeff-0.5.0.tar.gz
| Download URL | doeff-0.5.0.tar.gz |
|---|---|
| Size | 60.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9a63cccdccd41e0c8e5b8a10185469d87f9fa551314ca5f3cd802fd479632566
|
|
BLAKE2b-256 checksum How to use checksums |
fbc4f7bf201102c113210cf66bcd5e5c133daf1eb521034d2038b978310b2983
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / doeff-0.5.0-py3-none-any.whl
| Download URL | doeff-0.5.0-py3-none-any.whl |
|---|---|
| Size | 52.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b90e67ef23aeb294b89c34d389b1989ddc4a0559ad9b20f623eef3f46e819ae4
|
|
BLAKE2b-256 checksum How to use checksums |
9ef70d5b7dc3f8fcc24d9fecd6ddb3e98cdb779a71dd535ce94dd5551242697b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for doeff 0.5.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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A 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.