vacuous
Finds Python tests that pass no matter what your code does.
A test with no assertions, or one that only checks its own mocks, still shows up green and still counts as coverage. Those are the worst kind to have, because they cost you the thing tests are for — you find out the code is broken from somewhere other than your test suite.
$ vacuous check tests/
12 vacuous tests in 847 (1.4%)
tests/test_billing.py:41 no-assertions certain
└─ `test_refund_is_recorded` contains no assertions — it passes unless the code under test raises.
tests/test_users.py:118 swallowed-failure certain
└─ this assertion in `test_email_is_unique` is caught and discarded by the handler on line 121 — it can never fail the test.
tests/test_orders.py:92 patched-target-under-test likely
└─ `test_charge_card` replaces `charge_card` with a mock and then only asserts on that mock — the real `charge_card` never runs.
63 files scanned
It's a static analysis pass over tree-sitter, so it doesn't run your tests and doesn't need your dependencies installed. Whole repos take well under a second. No network, no API keys.
Install
uv tool install vacuous
or pipx install vacuous, or pip install vacuous. There are prebuilt wheels
for Linux, macOS and Windows, so this needs no Rust toolchain.
If you'd rather build it yourself:
cargo install vacuous
Needs Rust 1.88 or newer.
Usage
vacuous check [PATH] # defaults to the current directory
--min-confidence <LEVEL> # certain | likely | possible (default: likely)
--format <FORMAT> # pretty | json | sarif (default: pretty)
--baseline <FILE> # defaults to .vacuous-baseline.json if present
--no-baseline # report everything, baseline included
vacuous baseline [PATH] # record what's there today
Exits 0 when clean, 1 when it finds something, 2 if the tool itself fell
over — so CI can tell a real failure apart from a broken run.
Adding it to an existing project
Any codebase of any age will have findings already, and nobody is going to fix two hundred of them before their next commit. Record them once:
$ vacuous baseline
vacuous: recorded 144 findings in .vacuous-baseline.json
vacuous: `vacuous check` will now report only new ones
Commit that file. From then on vacuous check passes, and only fails when
someone adds a new test that can't fail.
Entries are keyed on the rule, file and test name, deliberately not the line
number, so editing a file doesn't invalidate the baseline. When findings get
fixed, vacuous says how many entries are stale and you can re-record.
pre-commit
repos:
- repo: https://github.com/MahdiAlani/vacuous-pre-commit
rev: v0.1.0
hooks:
- id: vacuous
Installs from the PyPI wheel, so contributors don't need Rust.
CI
- run: vacuous check --format sarif > vacuous.sarif
continue-on-error: true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: vacuous.sarif
Findings then show up as annotations on the pull request. certain maps to
error, likely to warning, possible to note; GitHub won't block merges
on any of them unless you configure it to.
Checks
| Check | What it means |
|---|---|
no-assertions |
Nothing in the test body can fail |
constant-assertion |
Every assertion is on literals: assert True, assertEqual(3, 3) |
swallowed-failure |
An except block discards the assertion |
unreachable-assertion |
The assertion sits after a return or raise |
patched-target-under-test |
The test mocks the function it's named for, then only checks the mock |
Each finding carries a confidence. certain means it's a structural fact about
the code; likely means there's a judgement call involved. Only certain and
likely show by default.
What it deliberately ignores
Tests without assertions aren't automatically bad, which is the main reason
pytest hasn't shipped a flag for this.
Getting the exceptions right is most of the work here, so vacuous stays quiet on:
- Tests that delegate to a helper that asserts, including one defined elsewhere
in the file and reached indirectly. Flask's
common_object_test(app)is the usual shape. - Assertion helpers that don't look like assertions —
eq_,is_,ne_and the rest of the nose/SQLAlchemy family. - Anything where a decorator does the asserting, like SQLAlchemy's
@profiling.function_call_count()or a@pytest.mark.benchmarkhanding off to the benchmark fixture. Plainpytest.marklabels don't count. - Tests returning a value, which means something else is driving them.
- Functions nested inside tests. Flask names route handlers
testand Click commandstestcmd; no runner collects those. assert False, "shouldn't get here". That's constant but always fails, so it's a deliberate marker, not a vacuous test. Ruff's PT015 and B011 cover that case from the other direction.except ValueError: passaround an assertion. AnAssertionErrorescapes it, so nothing is being swallowed.
Most of those came from running it over real suites and finding out it was wrong. It's currently checked against flask, requests, httpx, rich, click, black, scrapy, celery, pydantic, ansible, django-rest-framework and sqlalchemy — about 29,000 tests — with every finding read by hand. Roughly 2% of tests in those projects can't fail.
If you hit a false positive, that's a bug worth filing.
How it works
Walk for files a test runner would collect, respecting .gitignore. Parse each
one, pull out the test functions, run every check over them, sort by location.
Files are handled in parallel.
The Python-specific parts live behind one trait in
src/lang/mod.rs, so the checks themselves don't know what
language they're looking at. A check is a function from one parsed test to a list
of findings; they're in src/rules/. Every check has a pair of
fixtures under tests/fixtures/, one that should be flagged and one that
shouldn't.
Related tools
- mutmut and cosmic-ray answer a harder version of this question by mutating your code and re-running the suite. Far more thorough, and much slower. Worth reaching for when you want a real answer about a specific module.
- flake8-aaa lints tests for Arrange-Act-Assert structure, which catches some of the same shapes.
- ruff has a pytest ruleset worth turning on regardless, and covers always-false assertions.
- flake8-pytest-style for pytest idioms more broadly.
vacuous is meant to sit alongside these, not replace them.
Planned
Config in pyproject.toml, so per-project settings live where the rest of your
tooling is configured. Per-line suppression comments.
After that, a verify mode: stub out a function, run the tests, and see whether
anything notices. Scoped to a diff it's quick enough to be useful, and it answers
directly what this tool can only approximate by reading code.
TypeScript, eventually. The checks don't know they're looking at Python — that lives behind one trait — so it's a new adapter rather than a rewrite.
License
MIT or Apache-2.0, at your option.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vacuous-0.1.1.tar.gz.
File metadata
- Download URL: vacuous-0.1.1.tar.gz
- Upload date:
- Size: 41.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
554c165ec09019a26b58e008f40c25e4a046d98adc57161cdadf9e12b94adbb1
|
|
| MD5 |
f853a2b5ae455917917d3f772c858d65
|
|
| BLAKE2b-256 |
90bc74390e83f0d439fd680d85389bbb42b7d752924355b609f2ba163e7c0fed
|
Provenance
The following attestation bundles were made for vacuous-0.1.1.tar.gz:
Publisher:
release.yml on MahdiAlani/vacuous
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vacuous-0.1.1.tar.gz -
Subject digest:
554c165ec09019a26b58e008f40c25e4a046d98adc57161cdadf9e12b94adbb1 - Sigstore transparency entry: 2305682110
- Sigstore integration time:
-
Permalink:
MahdiAlani/vacuous@c00a7c50c5909859a9655e31e1277c9cc8504c0e -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/MahdiAlani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c00a7c50c5909859a9655e31e1277c9cc8504c0e -
Trigger Event:
push
-
Statement type:
File details
Details for the file vacuous-0.1.1-py3-none-win_amd64.whl.
File metadata
- Download URL: vacuous-0.1.1-py3-none-win_amd64.whl
- Upload date:
- Size: 1.4 MB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9f3db8fd2dcd18ba2fffed796e50da7feada96617bbfcea078c55e5ddb9e6beb
|
|
| MD5 |
86f4dd3d719e81cb50ae30490d5be81e
|
|
| BLAKE2b-256 |
aec1b6b117d27ef62bc1fe80c30397e2996528197c251ae3e7e4d8db41bf48b2
|
Provenance
The following attestation bundles were made for vacuous-0.1.1-py3-none-win_amd64.whl:
Publisher:
release.yml on MahdiAlani/vacuous
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vacuous-0.1.1-py3-none-win_amd64.whl -
Subject digest:
9f3db8fd2dcd18ba2fffed796e50da7feada96617bbfcea078c55e5ddb9e6beb - Sigstore transparency entry: 2305682298
- Sigstore integration time:
-
Permalink:
MahdiAlani/vacuous@c00a7c50c5909859a9655e31e1277c9cc8504c0e -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/MahdiAlani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c00a7c50c5909859a9655e31e1277c9cc8504c0e -
Trigger Event:
push
-
Statement type:
File details
Details for the file vacuous-0.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: vacuous-0.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 1.5 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
404c486933c3c4cccb9da8a9ed1ecd3eb38729e16da8052f6b3bab7852e5b18a
|
|
| MD5 |
64bfcc3afc4b7d3cd531c995a54a367f
|
|
| BLAKE2b-256 |
f5aa030723cf629e0d8677314510681053fb3590aa050bfefc6f1b85438bf6e3
|
Provenance
The following attestation bundles were made for vacuous-0.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yml on MahdiAlani/vacuous
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vacuous-0.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
404c486933c3c4cccb9da8a9ed1ecd3eb38729e16da8052f6b3bab7852e5b18a - Sigstore transparency entry: 2305682231
- Sigstore integration time:
-
Permalink:
MahdiAlani/vacuous@c00a7c50c5909859a9655e31e1277c9cc8504c0e -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/MahdiAlani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c00a7c50c5909859a9655e31e1277c9cc8504c0e -
Trigger Event:
push
-
Statement type:
File details
Details for the file vacuous-0.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: vacuous-0.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 1.4 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2c50a125b03c7604fc383b79650671211f46274f8a823e771d0a6780e1ac8176
|
|
| MD5 |
a2981fdeda7495c06df02ca58d59d0bf
|
|
| BLAKE2b-256 |
fbaca6f98b33681b1bb349fa48983f2550b54529bd405f7823d9a4434ea0dffd
|
Provenance
The following attestation bundles were made for vacuous-0.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
release.yml on MahdiAlani/vacuous
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vacuous-0.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
2c50a125b03c7604fc383b79650671211f46274f8a823e771d0a6780e1ac8176 - Sigstore transparency entry: 2305682411
- Sigstore integration time:
-
Permalink:
MahdiAlani/vacuous@c00a7c50c5909859a9655e31e1277c9cc8504c0e -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/MahdiAlani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c00a7c50c5909859a9655e31e1277c9cc8504c0e -
Trigger Event:
push
-
Statement type:
File details
Details for the file vacuous-0.1.1-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: vacuous-0.1.1-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.3 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6fa4574c11584ed562b7073c416f65782c9d3bbcb38b8357c77f04b56d0e4c17
|
|
| MD5 |
87ea4cdbfd90b13b3044edfa10fb62d8
|
|
| BLAKE2b-256 |
d371a55c63d537c561517c36632ff5b0dc9bad3b405412afc79070484f30b785
|
Provenance
The following attestation bundles were made for vacuous-0.1.1-py3-none-macosx_11_0_arm64.whl:
Publisher:
release.yml on MahdiAlani/vacuous
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vacuous-0.1.1-py3-none-macosx_11_0_arm64.whl -
Subject digest:
6fa4574c11584ed562b7073c416f65782c9d3bbcb38b8357c77f04b56d0e4c17 - Sigstore transparency entry: 2305682569
- Sigstore integration time:
-
Permalink:
MahdiAlani/vacuous@c00a7c50c5909859a9655e31e1277c9cc8504c0e -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/MahdiAlani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c00a7c50c5909859a9655e31e1277c9cc8504c0e -
Trigger Event:
push
-
Statement type:
File details
Details for the file vacuous-0.1.1-py3-none-macosx_10_12_x86_64.whl.
File metadata
- Download URL: vacuous-0.1.1-py3-none-macosx_10_12_x86_64.whl
- Upload date:
- Size: 1.4 MB
- Tags: Python 3, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
524ea32691bd06eb9fd72849352dca3c7303d9abd317bac3a22a995f18bb88c2
|
|
| MD5 |
7258f79a83fefcdd68255e374c463534
|
|
| BLAKE2b-256 |
369b75ec8042fe5eae971b5355aa0ca1295a5e9f5d4c60f8ac3aafbc54a39df5
|
Provenance
The following attestation bundles were made for vacuous-0.1.1-py3-none-macosx_10_12_x86_64.whl:
Publisher:
release.yml on MahdiAlani/vacuous
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vacuous-0.1.1-py3-none-macosx_10_12_x86_64.whl -
Subject digest:
524ea32691bd06eb9fd72849352dca3c7303d9abd317bac3a22a995f18bb88c2 - Sigstore transparency entry: 2305682484
- Sigstore integration time:
-
Permalink:
MahdiAlani/vacuous@c00a7c50c5909859a9655e31e1277c9cc8504c0e -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/MahdiAlani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c00a7c50c5909859a9655e31e1277c9cc8504c0e -
Trigger Event:
push
-
Statement type: