Home / How it works
A Rust front, a warm Python back
rpytest is a small Rust CLI over a long-lived Python daemon. The CLI is what your shell and CI call; the rpytest-daemon hosts a real pytest process and keeps the interpreter, plugins, and collection warm — so the expensive startup work is paid once per session, not once per run.
The architecture
your shell / CI / watch loop
│ rpytest -k auth -m "not slow" (pytest CLI surface)
▼
┌────────────────────────┐
│ rpytest CLI (Rust) │ parse flags · read config in place · select
└──────────┬─────────────┘
│ rpytest-ipc (RPC)
▼
┌────────────────────────┐
│ rpytest-daemon │ hosts REAL pytest · fixtures + plugins resident
│ (persistent Python) │ collection cached · interpreter warm
└──────────┬─────────────┘
▼
┌──────────────┐ -n auto ┌──────────────┐
│ filter cached │─────────▶│ run selected │──▶ results stream to CLI
│ node IDs │ │ (duration bal)│ exit code + JUnit == pytest
└──────────────┘ └──────────────┘
first run: import + collect once · every later run: a warm RPC
The flow, step by step
Nothing about your tests changes. The daemon simply moves the fixed cost — interpreter startup, plugin import, and collection — out of the per-invocation path.
- 1
CLI parses your command
The Rust CLI (crate rpytest) parses pytest-shape flags and selectors, reads config from pytest.ini / pyproject.toml / tox.ini / setup.cfg in place, and dispatches over IPC. Its resident footprint stays in single-digit megabytes.
- 2
IPC dispatches to the daemon
crate rpytest-ipc defines the message types between CLI and daemon. Each invocation is a small RPC rather than a fresh Python process launch.
- 3
The daemon hosts real pytest
A background Python process (crate rpytest-daemon) imports your suite once, caches the collected node IDs, keeps fixtures and plugins resident, and waits for RPCs.
- 4
Run, then stream results
The daemon filters the cached inventory, optionally fans out to workers with duration-aware balancing, runs only what was selected, and streams results back. Exit codes and JUnit XML match pytest exactly.
Why the daemon is the win
Plain pytest re-pays interpreter startup, plugin import, and test collection on every invocation. pytest-xdist re-pays worker startup on top. For suites with a non-trivial plugin graph, that fixed cost dominates wall clock before a single test body runs. rpytest pays it once per session and turns each later run into a small RPC into a warm interpreter.
Staying honest about parity
Because the daemon hosts real pytest, fixtures, conftest.py, parametrization, and hooks run exactly as they do in pytest. Exit codes and JUnit XML match; console stdout is similar but not byte-identical. Run rpytest --verify-dropin to diff collected node IDs, per-test outcomes, and exit codes against pytest — it tells you precisely where, if anywhere, the two disagree.
Ready to try it? Head to the quickstart, browse the full feature set, or see real use cases.