Skip to content
rpytest GitHub

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