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 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 main (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

--base defaults to the first of origin/main, main, master that exists; the diff runs from merge-base(HEAD, base) to the working tree, staged and unstaged alike. --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 main (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.1

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.1
File Size Uploaded Provenance Actions
gerenuk-0.5.1.tar.gz 1.1 MB With provenance Details for gerenuk-0.5.1.tar.gz

Built distributions (wheels)

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

Total release size: 4.9 MB

Release files / gerenuk-0.5.1.tar.gz

Download URL gerenuk-0.5.1.tar.gz
Size 1.1 MB
Tags Source
SHA-256 checksum
How to use checksums
43b5416a2cc48d34d9158c47ef6df804e5e9dfad44e28b729c40feb85e36de9c
BLAKE2b-256 checksum
How to use checksums
ac1cee51f633f65dcaa0bf66eadc0fb22acaa54018239207447a3d7952938a54
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 Oct 28, 2026.

Transparency log

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

Download URL gerenuk-0.5.1-py3-none-win_amd64.whl
Size 1.3 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
9cebbdff81105177f13df981b8a4ff019d11e35c8ed0ec94853e4cab79bea0a5
BLAKE2b-256 checksum
How to use checksums
cedf7d5e3475be83b83589a575ca3cd14cfeaf83ed7f598b770f2b1e0f89e1be
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 Oct 28, 2026.

Transparency log

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

Download URL gerenuk-0.5.1-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
91635642daf50492f4b582771bae00b64be347b92c96ba490e0ae45026bf6dec
BLAKE2b-256 checksum
How to use checksums
5687fc9397c2210d4cd8deca1992c20243940fb678013d359352bac8794aa90c
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 Oct 28, 2026.

Transparency log

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

Download URL gerenuk-0.5.1-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
6cc139f330507b6d95142a4d39ee87ee01d59d522a1a0dcd675d52788c8cca18
BLAKE2b-256 checksum
How to use checksums
83b8c22258d3c3bf8e976fc23b29824b78dd2c27ee7898de70fde632d88ab743
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 Oct 28, 2026.

Transparency log

Security for gerenuk 0.5.1

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.1.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 Oct 28, 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