Skip to main content

A fast, pytest-compatible test collector and runner, written in Rust.

Project description

cito

CI

A fast, pytest-compatible test collector and runner, written in Rust.

cito (Latin: "quickly" — the word doctors still write on urgent orders) makes the pytest inner loop fast, the way Ruff and uv did for linting and packaging. It discovers your tests by parsing them with ruff's parser — in milliseconds, not seconds — and verifies against real pytest that it finds the same node IDs.

Status: v0.2. Collection is differential-tested against ~40 real suites — pytest's own, pandas, home-assistant, sphinx, pip — plus 100 fuzz seeds; ~640k node IDs checked in total. The runner does parallel subprocesses, warm in-process workers, and a per-project daemon that makes one-shot runs ~0.02 s. Not yet 1.0; the compatibility contract below is the map. Pre-1.0 versioning: 0.x minors may change behavior where pytest compatibility requires it, patches are fixes only; 1.0 freezes the CLI and the node-ID contract.

Benchmarks

Collection, wall time (Apple M4 Max, Python 3.14, warm cache; see BENCHMARKS.md to reproduce):

suite tests pytest --collect-only -q cito collect speedup
home-assistant 2026.7.1 (validated scope) 81,251 16.91 s 0.11 s 156x
pandas 3.0.3 (installed) 197,077 9.48 s 0.26 s 36x
pytest 9.1.1 (own suite) 4,231 0.62 s <0.01 s >100x
synthetic corpus 11,000 0.70 s 0.01 s 70x

And the part that matters more than speed — the same answers:

suite pytest IDs missing wrong extras
pytest's own suite 4,231 1 (a .txt doctest; doctest support is a known gap) 0
home-assistant 2026.7.1 (core + 249 integrations) 81,251 0 0
pandas 3.0.3 197,077 26 (0.013%) 2
flask 3.1.3 482 0 0
rich 15.0.0 981 0 0
click 8.4.2 1,686 0 0
jinja2 3.1.6 909 0 0
attrs 26.1.0 1,386 0 0
httpx 0.28.1 1,418 0 0
starlette 1.3.1 981 0 0
urllib3 2.7.0 2,273 0 0
werkzeug 3.1.8 969 0 0
requests 2.34.2 633 0 0
more-itertools 11.1.0 722 0 0
packaging 26.2 61,513 0 0
pluggy 1.6.0 124 0 0
tornado 6.5.7 1,322 0 0
black 26.5.1 446 0 0
pydantic 2.13.4 12,775 0 0
fastapi 0.139.0 3,323 0 0
sympy 1.14.0 13,657 0 16 (0.1%: custom @SKIP import-time machinery)
typer 0.26.8 1,379 0 0
networkx 3.6.1 7,100 0 0
cryptography 49.0.0 4,472 0 0
django-rest-framework 3.17.1 1,552 0 0
sqlglot 30.12.0 1,127 0 0
pytest-asyncio 1.4.0 299 0 0
textual 8.2.8 3,467 0 0
pytest-xdist 3.8.0 212 0 0
httpcore 1.0.9 220 0 0
scikit-learn wheel (site-packages) 47,349 0 2
botocore 1.43.40 78,668 0 0
tox 4.56.1 7,929 0 0
openai-python 2.44.0 6,731 0 0
coverage.py 7.15.0 1,586 0 0
virtualenv 21.6.0 328 0 0
sphinx 9.1.0 2,424 0 0
pip 26.1.2 2,997 0 0
scipy wheel (site-packages) 96,387 3,467 (3.6%: type() class factories) 5
numpy 3.x wheel (site-packages) 49,443 760 (1.5%: type() loop-generated SIMD classes) 0
trio 0.33.0 895 0 0
pillow 12.3.0 5,219 0 0
aiohttp 3.14.1 4,364 0 0
hypothesis 6.156.1 3,647 0 3 (asyncio wrapper dynamics)

(scripts/validate_repos.py reruns the whole matrix against fresh clones — the release gate. sqlalchemy and django are documented out: their suites require project-specific collection-bootstrap plugins that no static tool can see.)

scripts/diff_collect.py computes this equivalence on every CI run.

Why

  • pytest is the default test runner of the Python world (~a billion downloads a month), and on large suites collection alone takes seconds to minutes (pytest#5516). Every pytest -k one_test pays that tax before a single test runs.
  • The tax is no longer only human: coding agents run the suite dozens of times per task. Suite latency is agent-loop latency.
  • Ruff and uv proved the recipe: reimplement the hot path in Rust, treat the existing ecosystem's behavior as a compatibility contract, win by 10–100x.

Install

Not on PyPI yet. From a checkout (builds with maturin or plain cargo):

$ uv tool install .          # or: pip install .
$ cargo install --path .     # Rust toolchain route

What works today

$ cito collect                    # pytest-convention discovery, in parallel
tests/test_api.py::test_get
tests/test_api.py::TestAuth::test_login[admin]
$ cito collect --count            # just the number
$ cito collect --json             # grouped by file, for tools and agents
$ cito collect --python .venv/bin/python   # env-aware: honors module-level importorskip
$ cito run -n 8                   # parallel runner (subprocess workers)
$ cito run -n 8 --warm            # v0.2 preview: pytest workers stay warm across chunks
$ cito run tests/test_api.py::TestAuth     # node-ID selectors, like pytest
$ cito run --lf                   # only the tests that failed last time
$ cito run --watch --warm         # live loop: warm workers survive across saves
$ cito run -k "http and not slow" # keyword expressions
$ cito run -x                     # stop at first failure (--maxfail N)
$ cito run --json                 # machine-readable summary for agents/CI
$ cito run -- --cov=mypkg         # pass anything through to pytest; parallel
                                  # coverage fragments are combined for you
$ cito run -m "not slow"          # mark expressions, filtered at collection time
$ cito run --changed              # only files whose content changed since last run
$ cito run --daemon               # hit the per-project warm daemon: one-shot
                                  # runs in ~0.02s (auto-starts; unix)
$ cito daemon status              # start | stop | status
  • Configuration discovery: pytest.ini, pyproject.toml ([tool.pytest] and [tool.pytest.ini_options]), tox.ini, setup.cfg; rootdir inference; testpaths, python_files / python_classes / python_functions (prefix and glob forms, including path patterns like testing/python/*.py), norecursedirs, virtualenv detection.
  • Collection semantics: definition-order node IDs; nested classes; __init__/__new__ exclusion; cross-module base-class resolution (the pandas TestMaskedArrays(base.ExtensionTests) pattern — resolved through imports, relative imports, star-imports, and Python's package-root sys.path semantics); unittest.TestCase subclasses collected regardless of naming.
  • Parametrize expansion with an honesty contract: literal scalars, tuples, stacked decorators (cartesian, pytest's piece order), ids=, class-level parametrize, module-level parametrize aliases, and duplicate-ID disambiguation are expanded exactly. Anything static analysis cannot prove — floats, computed values, indirect=, parametrized or autouse fixtures, pytest_generate_tests in scope, unknown decorators — falls back to the bare test name rather than risking a wrong ID. A bare name is always a valid pytest selector for all of its parametrizations.
  • Environment awareness (opt-in): --python PY probes module-level pytest.importorskip("...") requirements and drops modules pytest would skip in that environment. Without it, collection is fully static.
  • Runner preview: cito run partitions node IDs (whole files together, like xdist --dist loadfile) across N pytest subprocesses; --warm keeps workers alive and runs chunks via pytest.main() in-process — execution stays inside real CPython, so conftest, fixtures, and plugins keep working. Corpus numbers: serial pytest 2.48 s → cito run -n 8 1.24 s → --warm 1.20 s.
  • Scheduling: failures are recorded in .cito/lastfailed (rootdir); every run schedules previously-failed files first, then files whose content hash changed since the last run, then most-recently modified — the fastest possible time-to-first-signal. --changed runs only the changed files. --lf runs only the recorded failures (the cache clears as they pass). --watch keeps running: save a test file and only that file reruns.
  • Node-ID selectors: cito run tests/a.py::TestX and cito collect tests/a.py::test_y restrict to matching tests, including their parametrizations.
  • Mark expressions: -m "not slow" filters on statically-harvested marks (function, class chain, and module pytestmark) at collection time — deselected tests are never scheduled at all. -m/-k inside config addopts are honored (CLI wins). Per-parametrize pytest.param(marks=...) marks are not filtered (approximation).
  • Namespace collection: test classes/functions imported into a test module are collected there too (the urllib3-contrib rerun pattern); @pytest.fixture(name=...) renames are tracked; anyio's plugin-injected backend parametrization is detected and falls back safely.

The compatibility contract

pytest's node IDs are the interface:

  1. A cito ID with a [...] suffix must match a pytest ID exactly.
  2. A bare cito ID stands for pytest's parametrized IDs of the same base — cito's declared fallback wherever static analysis cannot be sure.

scripts/diff_collect.py enforces both directions on every commit against the fixture trees, a generated corpus, and randomized differential fuzzing (bench/fuzz_gen.py builds seeded projects mixing nested classes, cross-module inheritance, re-exports, parametrize variants, fixtures, marks, and shadowing; 100 seeds pass locally, three run in CI). ~40 real repositories — from flask to pandas to home-assistant — are checked before releases via scripts/validate_repos.py.

Known gaps, tracked honestly:

  • doctest collection (--doctest-modules, .txt doctests)
  • exact expansion where parametrization is computed at runtime (falls back to bare names by design; ~4% of IDs in heavily-fixtured repos like pandas), including pytest's duplicate-ID suffixes (True0/True1), which always fall back
  • pytest_generate_tests-generated extra tests that add new names
  • import-time class factories that synthesize tests from data files (jsonschema builds ~7k tests from the JSON-Schema-Test-Suite this way)
  • plugin-driven collection hooks and custom collectors (literal collect_ignore / collect_ignore_glob lists in conftest.py ARE supported; computed appends are not), and plugins that redefine collection semantics outright (pytest-relaxed)

Architecture

  1. v0.1 — collection parity + speed: shipped.
  2. v0.2 — warm workers: shipped — --warm pools within a run, --watch keeps them across saves, and cito run --daemon keeps them across CLI invocations (workers self-purge modules whose files changed).
  3. v0.3 — scheduling: mostly shipped (failed-first, content-changed-first, --lf, --changed, --json); remaining: AST-level impact analysis.
  4. Plugin compatibility matrix: explicit, tested support for the top-20 pytest plugins (xdist, cov, asyncio, django, hypothesis, mock, ...).

Non-goals

  • Replacing pytest's test-writing API. Your tests, fixtures, and plugins are the point; cito's job is to run them faster.
  • A new assertion or fixture DSL.

Development

$ cargo test                                   # unit + integration tests
$ cargo build --release
$ uv run --with pytest scripts/diff_collect.py tests/fixtures/basic
$ bench/bench_collect.sh                       # reproduce the corpus numbers

License

Licensed under either of Apache License 2.0 or MIT License at your option.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cito-0.2.0.tar.gz (84.5 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

cito-0.2.0-py3-none-win_amd64.whl (2.1 MB view details)

Uploaded Python 3Windows x86-64

cito-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.1 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

cito-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (2.0 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

cito-0.2.0-py3-none-macosx_11_0_arm64.whl (1.9 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

cito-0.2.0-py3-none-macosx_10_12_x86_64.whl (2.0 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file cito-0.2.0.tar.gz.

File metadata

  • Download URL: cito-0.2.0.tar.gz
  • Upload date:
  • Size: 84.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cito-0.2.0.tar.gz
Algorithm Hash digest
SHA256 4aa75ee11e9e039c5e424498166e084775a4460bc5cf0a56f1977167297cf0aa
MD5 25820e43ed9db775a227741649637d6a
BLAKE2b-256 5dab835a74a4d197184c5ad69c7198d52964bd5989afff6593fa765c1a564049

See more details on using hashes here.

Provenance

The following attestation bundles were made for cito-0.2.0.tar.gz:

Publisher: release.yml on IShinji/cito

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cito-0.2.0-py3-none-win_amd64.whl.

File metadata

  • Download URL: cito-0.2.0-py3-none-win_amd64.whl
  • Upload date:
  • Size: 2.1 MB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cito-0.2.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 9ebc8329350a72aa75be220ff1e98acc8a2354f238a6bb6fdeda4b6489d5a4f9
MD5 b91d65791eab5aedb9098894bd2cf242
BLAKE2b-256 9be03e8ec08a83bfb3163a6a65e390acbdf9fa84628488fcd800d99e35b6e31e

See more details on using hashes here.

Provenance

The following attestation bundles were made for cito-0.2.0-py3-none-win_amd64.whl:

Publisher: release.yml on IShinji/cito

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cito-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for cito-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 4e09bb0e3ae95b131f5e9bc7901b43831fca37e7348a1062d49f004e9a669c94
MD5 8043eb7257e857e3afbe8fad89ee2a65
BLAKE2b-256 1c36205e0bff10fc2422bc81b6bc17a6924c9299599ebb6f11ba530e58d13762

See more details on using hashes here.

Provenance

The following attestation bundles were made for cito-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on IShinji/cito

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cito-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for cito-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 e2af84199f3dec885437a3ef3dcbe54015621238a7980944faa0f19b147240d6
MD5 495e4f0fc6ead44f9a0422f1902c700f
BLAKE2b-256 bc650be4514960dd7f3dddbd0c8fa9aeaa5e620ecbac332c5e997e27521e26c2

See more details on using hashes here.

Provenance

The following attestation bundles were made for cito-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on IShinji/cito

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cito-0.2.0-py3-none-macosx_11_0_arm64.whl.

File metadata

  • Download URL: cito-0.2.0-py3-none-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 1.9 MB
  • Tags: Python 3, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cito-0.2.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 8d8ecf44e0bae6ebbd35321d1a23ae878faffdb0bcf4df37df86e02806cb6696
MD5 bfb9cdefbfcbe00f177e89c7c1907db5
BLAKE2b-256 7b000f42bfeb884f1cf0862152c85e7ce6852b3134e7f3136a431e0b02f7e202

See more details on using hashes here.

Provenance

The following attestation bundles were made for cito-0.2.0-py3-none-macosx_11_0_arm64.whl:

Publisher: release.yml on IShinji/cito

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cito-0.2.0-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for cito-0.2.0-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 63e8d632714c04d916b6b25d0f1c049ae9ac9dff456db6b9e3479fc4faa90acb
MD5 64b561a7ef7a588413208574208b6c18
BLAKE2b-256 968981ca7d2c68ad46ef777ce8427bab6a23fa20de343825519998f6895a4254

See more details on using hashes here.

Provenance

The following attestation bundles were made for cito-0.2.0-py3-none-macosx_10_12_x86_64.whl:

Publisher: release.yml on IShinji/cito

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page