ranit
Which functions in your diff were executed by nothing — including by the test you just wrote for them.
pip install ranit
coverage run -m pytest
That's it. ranit registers itself as a pytest plugin, so there is no new command to remember. When your suite finishes it either says nothing, or says this:
ranit: changed code that nothing executed
src/billing/refunds.py:88 function _apply_partial_credit
Three lines you just wrote, a green suite, and not one of them ran.
Why this exists
Linters prove your code parses. Type checkers prove it types. Coverage prints a percentage for a file. None of them tell you whether the function you just changed actually ran.
The gap shows up most sharply in a test that passes while executing nothing — mock out the subject, reimplement the logic in the test body, assert the mock you just created was called by yourself, print a green checkmark. It passes in 0.08s. Every tool above is satisfied.
ranit reads the coverage database your test run already produced, intersects it with your git diff and the AST, and reports per symbol.
What it says
| verdict | meaning | exit |
|---|---|---|
PROVEN |
every changed symbol executed | 0 |
REFUTED |
at least one changed symbol executed by nothing | 1 |
INSUFFICIENT |
not measurable — stale database, unmeasured files, no per-test contexts | 2 |
INSUFFICIENT is the point. It is a distinct third state, never folded into a pass. If the tool cannot see, it says so:
ranit: no coverage active — nothing was measured, so nothing was checked.
Run `coverage run -m pytest` (or `pytest --cov`) to get a verdict.
It prints that only when your diff actually contained something it would have judged. A clean tree, a comment-only change, or a trivial edit stays silent.
Usage
coverage run -m pytest # plugin reports automatically
pytest --cov=. # also works
ranit # check the current diff by hand
ranit --tests # which TESTS executed zero product lines
ranit --quiet # print only real findings
ranit --allow-stale # proceed on an older coverage database
ranit --install-hook # install a pre-commit hook
--tests needs per-test attribution, which needs a coverage rcfile:
[run]
dynamic_context = test_function
Note that coverage run --context=NAME sets one static label for the whole run and cannot attribute anything to an individual test.
It never changes your suite's exit status. A reporting plugin that can fail someone else's build is worse than no plugin.
Why not diff-cover, or Codecov patch coverage?
They answer a genuinely different question, and if line-level patch coverage is what you want, use them — they are good at it.
- diff-cover / Codecov patch coverage report what percentage of your changed lines are covered. The unit is the line, the output is a ratio, and the usual response to "87%" is nothing.
ranitreports which named functions in your diff executed nothing. The unit is the symbol, the output is a list of names, and the response to_apply_partial_credit ran zero timesis obvious and immediate.
A patch at 87% and a patch containing one entirely dead new function can be the same patch. The percentage is where that fact goes to hide.
Also: Codecov is a service with an account, a token and an upload. ranit is one dependency, runs locally, sends nothing anywhere, and needs no API key.
Why not vulture, or a dead-code finder?
vulture and friends are static — they infer from the AST whether a symbol looks used. ranit reads what actually executed. Static analysis cannot see a function reached only through a plugin registry, and it cannot see one that is referenced everywhere but never called.
Why diff-scoped
A repo-wide list of never-executed symbols runs to thousands of rows. Nobody acts on it, the same way nobody has acted on coverage's "Missing" column for thirty years. Scoped to the diff it becomes a fact with one obvious response, delivered while the code is still cheap to change.
Design
- One dependency (
coverage). No LLM, no vendor, no API key, no network, no telemetry. - Quiet by default. A plugin that speaks on every run gets disabled, and then it protects nothing.
- Never raises into the host. Every failure path is a silent return.
- Hunk-scoped. Editing one function in a 600-line module does not report every other dead symbol in that file as yours.
Receipts are written to .ranit/ only when you opt in via RANIT_HOME.
Honest limits
- It needs a coverage run. No coverage, no verdict — and it tells you rather than passing.
- Symbols under 3 lines are skipped; flagging one-line accessors trains people to ignore the report.
--testsneeds the rcfile above; without it you getINSUFFICIENT, not a guess.- It proves execution, not correctness. A function that ran is not a function that works.
Verification
ranit is checked against third-party repositories it has never seen, with positive and negative controls. Latest run: 32/32 conditions — eight controls across tenacity, click, markupsafe and attrs, zero configuration. The controls include a symbol referenced only by a test that never runs, a symbol dispatched dynamically so its name appears nowhere at the call site, and a symbol called from a branch the test never takes.
The check itself is tested, by running deliberately broken builds through it and requiring them to be rejected. Seven exist and all seven are rejected — five of them written specifically to defeat the controls that existed at the time, and three of those succeeded before a new control was added. The most recent passes seven of the eight controls and is caught only by the branch-coverage one.
That is the honest claim: the check catches those seven, not broken builds in general. Every fake is kept and re-run rather than retired, because a gate that no longer faces the thing it once failed has no evidence it still holds.
License
MIT © Nucleus Team
Metadata
Release files for ranit 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ranit-0.1.1.tar.gz | 27.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ranit-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 51.7 kB
Release files / ranit-0.1.1.tar.gz
| Download URL | ranit-0.1.1.tar.gz |
|---|---|
| Size | 27.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5d4d50f856190cf3f440b840c78585befc32232284d926f3be61fe45957bc971
|
|
BLAKE2b-256 checksum How to use checksums |
5e84daf5688a66fa1bb5c9cdc69d5fb91207bd9f8f42e1f0e204672c8c76a50c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.4
|
Release files / ranit-0.1.1-py3-none-any.whl
| Download URL | ranit-0.1.1-py3-none-any.whl |
|---|---|
| Size | 23.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
74e2a3caf42278f89de66d82bc54c3bf063571d508b4a1c9a8f467f4aa17a688
|
|
BLAKE2b-256 checksum How to use checksums |
6b602ecc39991f6806db4a0964c0429b0f42a8fecd7e68a6bf4f2c1bce12fad6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.4
|