Skip to main content

gerenuk

Impact-based pytest selection for Python, powered by ty-find.

A diff comes in; gerenuk run runs exactly the tests that diff impacts, and nothing else:

$ gerenuk run -- -q
gerenuk: 5 node id(s) from 1 origin(s) in 152 ms — details: gerenuk impacted-tests
.........                                                                [100%]
9 passed in 0.02s

It gets there by asking ty's type checker, through tyf, which symbols the working tree changed and which tests can reach them — so the selection follows Python's actual name resolution, not a name match. Anything gerenuk cannot see through becomes "run the whole suite" rather than a confident short list.

📖 Documentation · 🤖 llms.txt

Install

uv add --dev "gerenuk[ty]"

ty-find comes with gerenuk — tyf is what it drives. The [ty] extra adds ty itself; without it tyf falls back to uvx ty, which works whenever uv is around but fetches the checker on first use.

Verify the setup:

gerenuk doctor

Usage

gerenuk changed-symbols                # what the working tree changed
gerenuk impacted-tests                 # and which tests that reaches
gerenuk run -- -q                      # and run exactly those, under pytest
gerenuk run --dry-run                  # the decision and the argv, no pytest
gerenuk replay <SHA> --run            # a past commit's selection, with per-test timings
gerenuk audit pkg/module.py            # separately: what nothing references
gerenuk guide                          # agent-facing instructions for where you stand

Not sure where to start? uvx gerenuk guide prints the setup instructions without installing anything; once a madoqua step names gerenuk, the same command prints how to read a report. Every page is under 60 lines.

The three selection stages are one pipeline, and run computes the whole thing in-process — the first two are there to be read when a selection surprises you.

changed-symbols

Maps the working tree's diff to the Python symbols it changed — the first stage of impact-based test selection. It needs only git; no tyf, no ty, no Python environment.

$ gerenuk changed-symbols
base HEAD~1 (merge-base 5ddda1f)

changed symbols (2)
  modified  method    mypkg.pipelines.enrich:Enricher.run  src/mypkg/pipelines/enrich.py
  added     function  mypkg.utils:parse_date               src/mypkg/utils.py

module-level changes (1)
  mypkg.pipelines.enrich  src/mypkg/pipelines/enrich.py

By default the diff runs from HEAD~1 — the last commit — to the working tree, staged and unstaged alike. scope = "branch" under [tool.gerenuk] (or --scope branch) diffs the whole branch instead, from merge-base(HEAD, base) with the first of origin/main, main, master that exists; --base <REF> names the ref outright. --format json emits the same data for scripting. Registry decorators can be filtered out via [tool.gerenuk] ignore-decorators in pyproject.toml. See the documentation.

impacted-tests

Walks the reference graph from those changed symbols out to the tests that reach them — the selection, minus the pytest invocation, which is run's job.

$ gerenuk impacted-tests
base HEAD~1 (merge-base 5ddda1f)
verdict selected

impacted tests (2)
  tests/test_enrich.py::test_run_enriches
    ← mypkg.pipelines.enrich:Enricher.run
  tests/test_api.py::test_endpoint_returns_lines
    ← mypkg.api:enrich_endpoint ← mypkg.pipelines.enrich:Enricher.run

41 symbol(s) visited, 5 tyf call(s), 640 ms

The ← chain is the point: when a selection looks wrong, it names the edge to blame, and tyf refs <symbol> confirms it by hand.

Every run emits a verdict. selected means the list is the answer; run_all means run the whole suite, with a machine-readable reason — a non-Python file changed, a file did not parse, tyf was unavailable or failed, or a budget tripped. Both exit 0. Under-selecting silently is the one outcome that would make the tool worse than not having it, so anything gerenuk cannot see through becomes "run everything" rather than a short list.

See the documentation.

run

Turns that selection into a pytest invocation and runs it. gerenuk says one line for itself and then becomes pytest, so Ctrl-C, colours and the exit code are pytest's own.

$ gerenuk run -- -q
gerenuk: 5 node id(s) from 1 origin(s) in 152 ms — details: gerenuk impacted-tests
.........                                                                [100%]
9 passed in 0.02s

gerenuk owns the invocation rather than printing node ids for a shell to interpolate, because a selection has three answers and an argument list has only two: an empty pytest argv is "run everything". So pytest $(gerenuk …) would run the entire suite in precisely the best case — the diff that impacts no tests. Here that case spawns nothing and exits 0.

run is also where pytest's own rules get applied. A recorded symbol is often a helper or a fixture rather than a test, so each name is checked against pytest's collection conventions and trimmed — or widened to the whole file — until it is one pytest will accept. And because pytest injects fixtures by name, which no type checker can follow, conftest.py is parsed for its fixtures and their consumers; the expansion keeps its audit trail:

tests/test_service.py::test_summary
  ← tests.conftest:shelter ← sample_pkg.service:describe

--dry-run prints the decision and the exact argv without spawning anything. pytest-command = ["uv", "run", "pytest"] under [tool.gerenuk] names the runner. See the documentation.

Dead code: audit

The same reference graph answers the opposite question — what does nothing reach? — so gerenuk audit asks it for every symbol in a file:

$ gerenuk audit sample_pkg/service.py
warn  sample_pkg/service.py:43  func `legacy_export` has no references
note  sample_pkg/service.py:34  method `ShelterService.seniors` is referenced only from tests (1)

1 file(s), 7 symbol(s) checked — 1 warn, 1 note
Severity Rule
warn The symbol has no references anywhere
note Every reference lives in a test file

Only callable symbols are audited; _private and dunder names are skipped. --format json emits the findings for scripting.

It is a verifier, not a sweep

vulture is the cheap repo-wide sweep for dead code: one pass over a whole project, no type checker involved. It works on names, so it cannot tell two same-named symbols apart, it flags dynamic and framework code that is very much alive, and it cannot tell you who references a symbol.

gerenuk audit asks ty's type checker instead, through tyf. References resolve the way Python resolves them — docstrings, comments and same-named symbols in other modules do not count — and each one comes back as a file and a line you can open. That costs one tyf refs call per file and needs tyf installed, which is why it takes the files you name rather than a whole repository. It is shaped for confirming a specific suspicion.

The two fit together in that order:

vulture src/                    # sweep: what might be dead
gerenuk audit src/suspect.py    # confirm the file against resolved references
tyf refs one_symbol             # or confirm a single name
# then delete

Exit codes carry the verdict: 0 nothing flagged, 1 findings reported, 2 the run could not complete. The split between 1 and 2 is what makes it usable in CI — a failing check and a broken setup are different problems.

Referenced only from tests

The note is the finding a name-based scanner cannot produce at all: every reference to the symbol exists, and every one of them is in a test file. Production stopped calling it and its own tests are what keep it alive — usually the residue of an unfinished refactor, and exactly the code that survives a dead-code sweep forever. That finding is why audit earns a place next to a repo-wide scanner rather than deferring to one entirely.

What it cannot see

gerenuk reports static references, plus the three edges a type checker cannot draw and gerenuk models explicitly: conftest.py fixtures, registering decorators, and renaming imports (from x import y as z, which tyf answers for under y while the code says z). Everything else dynamic — registries populated at runtime, getattr dispatch, __all__ star re-exports — stays invisible.

So findings are leads, not a delete list. Confirm with tyf refs <symbol> before deleting anything.

Registered functions

Nothing references a @app.command() or a @router.get() — the framework holds the only handle. So gerenuk follows the decorator to its registrar (app, router) and treats what reaches the registrar as reaching the function. A decorator that only wraps (@property, @functools.wraps) is left alone, and a registrar that cannot be resolved gives run_all with reason: "decorator_dispatch" rather than a confident empty answer. See ADR 0012.

Usage with Claude Code

Add this to your project's CLAUDE.md. Two lines is the whole of it — the exit codes, the run_all verdict and the caveat above are in gerenuk --help and on the documentation site, which is where an agent that needs them should go:

### `gerenuk` — test selection and dead code

- `gerenuk run -- -q` runs only the tests the working tree's diff impacts
  (`--dry-run` to inspect; `gerenuk impacted-tests` explains why).
- `gerenuk audit <file>` confirms a symbol vulture flagged is really unused;
  its `only tests reach it` findings are ones vulture cannot produce.

Development

make review        # fmt + clippy + rust tests + fixture pytest + audit + deny
make review-quick  # skip the network checks
make test-impact   # impacted-tests against the fixture, with a real tyf
make test-run      # the whole pipeline, with a real tyf and a real pytest
make docs          # build the mdBook site and llms.txt into docs/book/html

The test suite is hermetic: tests/common/mod.rs stubs tyf and pytest with shell scripts and points GERENUK_TYF and GERENUK_PYTEST at them, so none of tyf, ty or pytest is needed to run cargo test. See docs/dev/ARCHITECTURE.md for the shape and docs/adr/ for why it is that shape.

License

MIT

Metadata

Release files for gerenuk 0.5.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gerenuk 0.5.5
File Size Uploaded Provenance Actions
gerenuk-0.5.5.tar.gz 1.1 MB With provenance Details for gerenuk-0.5.5.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for gerenuk 0.5.5
File Interpreter ABI Platform Actions
gerenuk-0.5.5-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details for gerenuk-0.5.5-py3-none-win_amd64.whl
gerenuk-0.5.5-py3-none-manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64 Details for gerenuk-0.5.5-py3-none-manylinux_2_28_x86_64.whl
gerenuk-0.5.5-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details for gerenuk-0.5.5-py3-none-macosx_11_0_arm64.whl

Total release size: 5.0 MB

Release files / gerenuk-0.5.5.tar.gz

Download URL gerenuk-0.5.5.tar.gz
Size 1.1 MB
Tags Source
SHA-256 checksum
How to use checksums
f450cbd4721b91f991c94278ab0f1b682098b0bb56a6a73a1ca0e55a393e18f3
BLAKE2b-256 checksum
How to use checksums
4c0c808fa5b1469711b6e647978caf72a34fcf751ef937be29f906842c7db285
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.

Transparency log

Release files / gerenuk-0.5.5-py3-none-win_amd64.whl

Download URL gerenuk-0.5.5-py3-none-win_amd64.whl
Size 1.3 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
598cfbc98b831aab63d007de11aa0df5ab7582771b71ae07d5698d9ea439fd87
BLAKE2b-256 checksum
How to use checksums
df7db0ed1d21fcc79361dae09e30488f34b68f06a58639db6dac4ea5c1321521
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.

Transparency log

Release files / gerenuk-0.5.5-py3-none-manylinux_2_28_x86_64.whl

Download URL gerenuk-0.5.5-py3-none-manylinux_2_28_x86_64.whl
Size 1.4 MB
Tags Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
f7a7e90e3c7badd212bec0ff139456afd3503a2efc224fbc1c184ff73416e30d
BLAKE2b-256 checksum
How to use checksums
e8a5f90b0e49db1c04b1c2258e3cff4a8dce22dd9ab185e94150299a7e8a88d8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.

Transparency log

Release files / gerenuk-0.5.5-py3-none-macosx_11_0_arm64.whl

Download URL gerenuk-0.5.5-py3-none-macosx_11_0_arm64.whl
Size 1.2 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
e9dca49e32962700d2a4a1240068ebc9d89b55536e87bd1296ff3de0639418e1
BLAKE2b-256 checksum
How to use checksums
81be2be37ee924e21f3df4cf5cff8b7db886d1a3f7da0057f6d74ad66f6e223f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.

Transparency log

Security for gerenuk 0.5.5

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 4 files in this release have been uploaded with provenance metadata.

Select from 4 files

Attestation for gerenuk-0.5.5.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page