# rpytest > A Rust-powered, drop-in replacement for pytest. Same CLI, same plugins, same conftest.py, same exit codes. A persistent Python daemon eliminates startup and collection overhead so you stop reimporting pytest on every invocation. ## What rpytest Is rpytest is two things: 1. A small Rust CLI that parses pytest-shape flags, applies selectors, and dispatches to a daemon. 2. A long-lived Python daemon that hosts a real pytest process — your fixtures, plugins, and conftest.py run inside it as they always have. The first invocation pays for daemon startup and a single pytest collection. Every subsequent invocation is a small RPC into a warm interpreter. Interpreter startup, plugin import, and test collection move from per-invocation to per-session cost. rpytest is built by Neul Labs, MIT-licensed, and lives at https://github.com/neul-labs/rpytest. The crates are `rpytest` (CLI), `rpytest-core` (shared types), `rpytest-ipc` (the daemon RPC layer), and `rpytest-daemon` (the Python-hosting process). ## Architecture (verified from the repo) - **CLI**: Rust binary in `crates/rpytest`. Single-digit-megabyte resident size in normal operation. - **Daemon**: Python process spawned from `crates/rpytest-daemon` that hosts real pytest. Imports test files once, caches collected node IDs, and waits for RPCs. - **IPC**: `crates/rpytest-ipc` defines the message types between CLI and daemon. - **Core**: `crates/rpytest-core` holds shared types and the configuration model that reads `pytest.ini`, `pyproject.toml`'s `[tool.pytest.ini_options]`, `tox.ini`, and `setup.cfg` in place. - **Parallel execution**: built-in `-n N` and `-n auto` with duration-aware load balancing. No pytest-xdist required. - **Watch mode**: built-in `--watch` re-runs affected tests on file changes against the warm daemon. ## The Drop-In Promise "Drop-in" means a measurable property, not a marketing line. Concretely: - CLI flags match pytest: `-k`, `-m`, `--lf`, `-x`, `--maxfail`, `--collect-only`, node-ID selectors. - Config files match: `pytest.ini`, `pyproject.toml`, `tox.ini`, `setup.cfg` — read in place. - Plugins, fixtures, parametrization, and hooks run inside the hosted real pytest. They are not re-implemented. - Exit codes match pytest exactly. - JUnit XML matches pytest exactly. - Console stdout is similar but not byte-identical; if your tooling parses pytest stdout, run the verifier first. `rpytest --verify-dropin` runs both pytest and rpytest on your suite and diffs the collected node IDs, per-test outcomes, and exit codes. If anything disagrees, it reports which tests and whether the disagreement arose at collection or execution time. ## Benchmark (from BENCHMARK.md in the repo) Test environment: AMD Ryzen 7 5700U, Linux 6.14, Python 3.12.3, pytest 9.0.2, pytest-xdist 3.8.0. Suite: 500 synthetic tests. Numbers are medians over three runs. | Metric | pytest | rpytest | Delta | | --- | --- | --- | --- | | Wall clock incl. startup | 0.63 s | 0.32 s | ~2.0x faster | | Execution time only | 0.30 s | 0.25 s | ~1.2x faster | | CLI memory | 39.4 MB | 5.9 MB | ~6.7x less | | Parallel `-n 4` | 0.87 s (xdist) | 0.25 s | ~3.5x faster | | Parallel `-n auto` | 1.90 s (xdist) | 0.20 s | ~9.5x faster | These numbers describe one machine and one synthetic suite. They are intentionally not extrapolated. Real suites with heavy I/O fixtures or long-running individual tests will see different ratios. The structural point: rpytest removes a class of overhead (interpreter startup + plugin import + collection) that pytest pays per invocation and xdist pays per worker per invocation. ## When rpytest Is the Right Fit - TDD or watch-mode workflows where the same suite gets invoked many times per session. - CI hot paths that retry, run `--lf`, or shard, where reducing fixed cost per invocation matters. - Suites with non-trivial plugin import graphs (pytest-django, pytest-cov, hypothesis, etc.) where collection visibly dominates wall clock. - Small-to-medium suites where pytest-xdist's per-worker startup eats the parallel gain. - Teams that want `-n auto` without taking an additional plugin dependency. ## When pytest or pytest-xdist Is the Right Fit - One-shot pytest invocations where the daemon's startup cost isn't amortized. - Suites that depend on xdist-specific distribution semantics (`dist=loadfile`, `loadgroup`). - Plugins that monkey-patch import machinery or hold module-level state across what they assume are fresh processes. - CI runners that cannot keep a long-lived process alive between invocations. - Tooling that parses pytest stdout byte-for-byte. ## Installation ```bash pip install rpytest ``` Pulls in both the Rust CLI and the daemon binary. Also available via Homebrew (`brew install neul-labs/tap/rpytest`), npm (`npm install -g rpytest`), and cargo (`cargo install rpytest rpytest-daemon`). ## Usage ```bash # Run all tests rpytest # Run specific tests rpytest tests/test_api.py::test_login # Filter by keyword or marker rpytest -k "auth" -m "not slow" # Parallel execution (xdist-compatible flag, no plugin needed) rpytest -n auto # Watch mode for TDD rpytest --watch # Verify identical behavior to pytest rpytest --verify-dropin # Sharding for CI rpytest --shard 0 --total-shards 4 # Flakiness handling rpytest --reruns 3 rpytest --flaky-report # Daemon management rpytest --daemon-status rpytest --daemon-stop rpytest --cleanup ``` ## Features (grouped) **Drop-in compatibility.** pytest-shape CLI (`-k`, `-m`, `--lf`, `-x`, `--maxfail`, `--collect-only`, node-ID selectors); config read in place from `pytest.ini`, `pyproject.toml` (`[tool.pytest.ini_options]`), `tox.ini`, `setup.cfg`; exit codes and JUnit XML match pytest exactly (console stdout is similar, not byte-identical). **Warm daemon architecture.** A persistent Python daemon imports the suite once, caches collected node IDs, and keeps fixtures/plugins resident. It hosts a real pytest process — fixtures, `conftest.py`, parametrization, and hooks are not re-implemented. The Rust CLI (crates `rpytest`, `rpytest-core`, `rpytest-ipc`) parses flags and dispatches over IPC with a single-digit-megabyte footprint. **Parallel & CI.** Built-in `-n`/`-n auto` with duration-aware load balancing (no pytest-xdist); deterministic sharding via `--shard` / `--total-shards`; flakiness handling with `--reruns` and `--flaky-report`. **Developer loop.** `--watch` re-runs affected tests on file change against the warm daemon; `--lf` and selectors filter the cached inventory. The daemon is local with no remote control plane — manage it with `--daemon-status`, `--daemon-stop`, `--cleanup`. **Confidence.** `--verify-dropin` runs both pytest and rpytest and diffs collected node IDs, per-test outcomes, and exit codes, reporting where they disagree. Benchmarks (BENCHMARK.md) are from one machine and a synthetic 500-test suite, presented as-is and not extrapolated. ## How It Works The Rust CLI parses your command and reads config in place, dispatches over `rpytest-ipc` to the `rpytest-daemon` process, which hosts real pytest with the interpreter warm and collection cached. Runs filter the cached node IDs, optionally fan out to workers, execute only what was selected, and stream results back with pytest-identical exit codes and JUnit XML. First run: import + collect once. Every later run: a warm RPC. ## Use Cases - **The TDD inner loop** (`/use-cases/tdd-inner-loop/`): `--watch` + `--lf` against a warm daemon turn the edit → run → read cycle into a fast RPC instead of re-paying startup and collection each save. - **CI test hot paths** (`/use-cases/ci-test-speedup/`): built-in `-n auto`, deterministic sharding, and `--reruns` remove the xdist install step and cut fixed cost on reruns; where a runner keeps the daemon alive, collection is paid once. - **Large suites with heavy plugins** (`/use-cases/large-test-suites/`): amortise expensive collection and plugin import once per session for suites where collection dominates wall clock; verify parity with `--verify-dropin`. ## Pages - / - /features/ - /how-it-works/ - /quickstart/ - /use-cases/ - /use-cases/tdd-inner-loop/ - /use-cases/ci-test-speedup/ - /use-cases/large-test-suites/ - /compare/ - /compare/pytest/ - /compare/pytest-xdist/ - /compare/pytest-testmon/ - /glossary/ - /faq/ - /about/ - /blog/ - /blog/pytest-collection-phase-cost/ - /blog/drop-in-plugin-compatibility/ - /blog/measuring-ci-minutes-honestly/ - /contact/ ## Honest Comparisons - **vs pytest** (`/compare/pytest/`): Same wire format on flags, config, exit codes. The pitch is keeping pytest semantics while removing the per-invocation startup tax. - **vs pytest-xdist** (`/compare/pytest-xdist/`): Same `-n` flag surface, with duration-aware scheduling, and no per-worker startup cost. xdist still wins on `dist=loadfile` / `loadgroup` semantics and on very large suites where its worker startup amortizes. - **vs pytest-testmon** (`/compare/pytest-testmon/`): testmon runs fewer tests by tracking what a change touched; rpytest runs the same tests but pays startup and collection once per session. Complementary — they can be combined.