Drop-in CLI
rpytest mirrors pytest's CLI exactly. Same flags, same selectors, same exit codes. `pytest.ini`, `pyproject.toml`, and `tox.ini` are read in place. If you know pytest, you know rpytest.
Rust CLI · Python daemon · same conftest.py
rpytest is a Rust-powered, drop-in replacement for pytest. Same CLI, same flags, same plugins, same exit codes — but a persistent Python daemon keeps the interpreter warm so you stop paying startup and collection cost on every run.
# Install and run — that's it
$ pip install rpytest
$ rpytest
# Same flags you already know
$ rpytest tests/test_api.py::test_login -k auth -m "not slow"
$ rpytest -n auto # built-in parallel
$ rpytest --watch # TDD loop
$ rpytest --verify-dropin # prove equivalence What is rpytest?
rpytest is a Rust-powered, drop-in replacement for pytest. It is a small Rust CLI plus a persistent Python daemon: the daemon hosts a real pytest process, so your fixtures, conftest.py, parametrization, and plugins run exactly as they always have.
The first invocation pays for daemon startup and a single collection. Every run after that is a small RPC into a warm interpreter — so interpreter startup, plugin import, and test collection move from per-invocation to per-session cost. Same CLI, same flags, same config files read in place, same exit codes.
At a glance
--verify-dropin proves parity.Same command, warm result
The invocation is identical. The difference is where the fixed cost goes — pytest re-pays it every run; rpytest pays it once and turns later runs into a warm RPC.
$ pytest -k checkout == interpreter startup == == importing plugins: django, cov, hypothesis == == collecting 1240 items == 3 passed in 1.9s # …and it re-pays all of that on the next run
$ rpytest -k checkout [daemon warm · collection cached] 3 passed in 0.11s # exit code + JUnit XML identical to pytest
Illustrative of the daemon model, not a benchmark. Measured figures live in the strip below and in BENCHMARK.md; prove your own with --verify-dropin.
Benchmark · from the repo
These figures come from BENCHMARK.md in the rpytest repository: a 500-test synthetic suite on an AMD Ryzen 7 5700U, Python 3.12.3, pytest 9.0.2, pytest-xdist 3.8.0. Your suite is not this suite. Run --verify-dropin and measure your own.
Wall clock, incl. startup
0.32 s
rpytest, 500 tests. pytest baseline: 0.63 s.
CLI memory
5.9 MB
rpytest CLI process. pytest baseline: 39.4 MB. Daemon adds ~80 MB shared.
Parallel vs xdist
0.25 s
rpytest -n 4. pytest -n 4 (xdist): 0.87 s. Worker startup is the gap.
Numbers from a single hardware profile and a synthetic suite. Real suites with heavy fixtures or I/O will show different ratios. The point isn't a single multiplier — it's that the daemon model removes a class of overhead that xdist re-pays per worker.
What you get
rpytest mirrors pytest's CLI exactly. Same flags, same selectors, same exit codes. `pytest.ini`, `pyproject.toml`, and `tox.ini` are read in place. If you know pytest, you know rpytest.
First invocation spawns a background daemon that imports your suite once. Every subsequent run is a fast RPC into a warm interpreter — no startup, no re-import, no re-collection.
`rpytest -n auto` runs your tests across workers with duration-aware load balancing. No pytest-xdist plugin required, though the flag surface matches.
`rpytest --watch` re-runs affected tests on file changes, reusing the warm daemon. The inner TDD loop drops from seconds to subseconds for most suites.
`--reruns N` retries failing tests, `--flaky-report` summarises which tests are quietly unreliable. Catch flake before it lands in your CI dashboards.
`--shard 0 --total-shards 4` splits a suite deterministically across CI jobs. Combine with the daemon model and per-shard collection cost effectively disappears.
`rpytest --verify-dropin` runs both pytest and rpytest on your suite and compares collection counts, results, and exit codes. Ship the swap with evidence, not vibes.
How it works
01 · CLI
Parses flags, applies selectors, dispatches to the daemon over IPC. Memory footprint stays in single-digit megabytes.
02 · daemon
A background Python process imports your suite once, caches the test inventory, and keeps fixtures/plugins resident.
03 · run
Each invocation is a small RPC. The daemon filters the inventory, optionally fans out to workers, and runs only what changed.
04 · stream
Test results stream to the CLI in real time. Exit code and JUnit XML match pytest exactly.
Honest comparisons
Three comparisons we'll defend: the baseline you already run, the parallel-pytest standard, and change-based selection. All tools we respect.
The baseline. We are wire-compatible on flags, config, and exit codes. The pitch is that you keep pytest semantics and remove the startup tax.
The standard way to parallel-run pytest. rpytest ships built-in `-n` with duration-aware scheduling and skips xdist's per-worker startup cost.
testmon runs fewer tests by tracking what your change touched. rpytest runs the same tests but pays startup and collection once per session — complementary approaches you can even combine.
FAQ
No. rpytest is a Rust CLI plus a Python daemon. The daemon hosts a real pytest process, so your fixtures, conftest.py, parametrization, and plugins all run in real pytest. The CLI handles dispatch, filtering, parallelism, and result streaming.
In general, yes — the daemon hosts pytest, so plugins load and run as they always have. Some plugins assume a fresh Python process per invocation; those are the most likely friction. The drop-in verifier flags differences for you.
Two sources. (1) The daemon keeps Python warm, so you skip interpreter startup, plugin import, and test collection on every invocation. (2) The Rust CLI does filtering, parallel dispatch, and result aggregation without paying Python overhead. The published benchmark in the README shows 1.9x wall-clock on a 480-test suite; mileage varies with suite shape.
For parallel test execution, yes — `rpytest -n auto` ships built-in with duration-aware scheduling, no plugin install required. xdist remains a fine choice when you want xdist specifically; we are not trying to retire it.
`pip install rpytest`. The pip package pulls in both the Rust CLI and the daemon binary. There are also Homebrew, npm, and cargo install paths in the README for non-pip environments.
Exit codes and JUnit XML match pytest exactly, by design. Console output is similar but not byte-identical — if your tooling parses pytest stdout, run the drop-in verifier on a representative suite first.
Yes. The CI hot path is one of the design targets: the daemon model pays off most on small re-runs (last-failed, single-file, watch), but sharding (`--shard`) and built-in parallelism also remove the xdist installation step. The "Measuring CI minutes saved, honestly" blog post walks through the numbers.
The daemon is a separate long-lived process; manage it with `rpytest --daemon-status`, `rpytest --daemon-stop`, and `rpytest --cleanup`. The daemon is intentionally local — there is no remote control plane.
Explore the docs
Jump straight into the guides, references, and write-ups — no digging through menus.
The full feature set by category — drop-in compatibility, warm daemon, parallel/CI, developer loop, and confidence tooling.
Rust CLI, IPC, and the daemon hosting real pytest — walked through with a pipeline diagram.
Install via pip, Homebrew, npm, or cargo; run like pytest; verify parity and manage the daemon.
The TDD inner loop, CI hot paths, and large suites with heavy plugins — three concrete workflows.
Collection phase, warm daemon, conftest.py, fixtures, IPC, and the rest of the vocabulary defined.
Drop-in guarantees, plugins and fixtures, exit-code parity, daemon management, CI, and how it stacks up.
The drop-in thesis and why the daemon architecture is the honest way to make pytest fast.
Collection cost, plugin compatibility, and an honest methodology for measuring CI minutes saved.
Honest, defensible comparisons against pytest, pytest-xdist, and pytest-testmon.
Install, run, and have the same tests pass in a fraction of the time. If they don't, --verify-dropin will tell you why before you commit.