Skip to content
rpytest GitHub

Rust CLI · Python daemon · same conftest.py

Run your pytest suite faster. Change nothing.

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 + 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?

A pytest that keeps the interpreter warm.

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

  • Drop-in — same CLI, flags, config, and exit codes as pytest.
  • Plugins untouched — real pytest hosts your fixtures and plugins.
  • Warm daemon — startup + collection paid once per session.
  • Built-in -n auto — parallel runs with no pytest-xdist.
  • Verifiable--verify-dropin proves parity.
  • MIT-licensed — Rust CLI, Python daemon, open source.

Same command, warm result

Nothing to relearn. It is pytest.

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 — cold every run
$ 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 — warm after the first 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

Honest numbers, not marketing math.

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

A pytest you don't have to migrate to.

Compatibility

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.

Architecture

Persistent Python daemon

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.

Parallel

Built-in parallelism

`rpytest -n auto` runs your tests across workers with duration-aware load balancing. No pytest-xdist plugin required, though the flag surface matches.

Developer loop

Watch mode for TDD

`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.

Reliability

Flakiness controls

`--reruns N` retries failing tests, `--flaky-report` summarises which tests are quietly unreliable. Catch flake before it lands in your CI dashboards.

CI

CI sharding

`--shard 0 --total-shards 4` splits a suite deterministically across CI jobs. Combine with the daemon model and per-shard collection cost effectively disappears.

Safety

Drop-in verification

`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

A Rust front, a warm Python back.

01 · CLI

Rust binary

Parses flags, applies selectors, dispatches to the daemon over IPC. Memory footprint stays in single-digit megabytes.

02 · daemon

Persistent pytest

A background Python process imports your suite once, caches the test inventory, and keeps fixtures/plugins resident.

03 · run

RPC, then execute

Each invocation is a small RPC. The daemon filters the inventory, optionally fans out to workers, and runs only what changed.

04 · stream

Results back

Test results stream to the CLI in real time. Exit code and JUnit XML match pytest exactly.

Honest comparisons

Where rpytest fits in your test toolchain

Three comparisons we'll defend: the baseline you already run, the parallel-pytest standard, and change-based selection. All tools we respect.

FAQ

Questions teams ask before they swap.

+ Does rpytest re-implement pytest?

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.

+ Do my plugins still work?

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.

+ How is it faster?

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.

+ Does it replace pytest-xdist?

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.

+ How do I install 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.

+ Will it change my test output?

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.

+ Can I run it in CI?

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.

+ Where does state live between runs?

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

Everything, one click deep.

Jump straight into the guides, references, and write-ups — no digging through menus.

Stop reimporting pytest on every save.

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.