assert-python-definition-is-used
Assert that every public Python definition in a tree is named somewhere else.
Why
A function whose last caller is deleted does not disappear. Its tests still pass, the coverage gate over its module stays green, and static analysis keeps reading it, so the tree grows a layer of code that is maintained and never called. Nothing in a normal toolchain reports it: coverage measures whether lines run, not whether anything wants them, and a module held at 100% by its own tests manufactures exactly the evidence that makes it look used.
This tool asks the other question. For every public top-level def and
class in the trees you point it at, it asks whether anything else
names it, and reports the ones nothing does.
Installation
pip install assert-python-definition-is-used
Usage
# Every definition in lib/python must be named somewhere in the repo
assert-python-definition-is-used lib/python \
--search-in lib/python --search-in scripts \
--search-in src --search-in test
# The same, but a module's own tests no longer count as a caller
assert-python-definition-is-used lib/python \
--search-in lib/python --search-in scripts \
--search-in src --search-in test \
--dont-search-in 'test/lib/python/test_{package}'
Those two runs are the pair worth having. The first is a cheap outer bound that catches a definition whose tests were deleted along with its caller. The second is the one that finds code kept alive only by the tests written for it, which is what a coverage gate hides.
Options
| Option | Effect |
|---|---|
--search-in PATH |
A tree to search for uses. Repeatable. |
--dont-search-in TEMPLATE |
Template left out of the search. |
--exclude PATTERNS |
Comma-separated globs to leave out of both trees. |
--assume-used-matching PATTERNS |
Globs of names a runtime invokes. |
--assume-used-decorated-with PATHS |
Decorators a runtime invokes. |
--quiet |
Print nothing; report through the exit code. |
--count |
Print only how many findings there were. |
--verbose |
Print the trees read, each file scanned, and a summary. |
--fail-fast |
Stop at the first finding. |
--warn-only |
Always exit 0. |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Nothing unused |
| 1 | Something unused |
| 2 | A tree was missing, unreadable, or would not parse |
What counts as a use
A use is the definition's name written as a whole word in any file the searched trees reach, apart from the file the definition lives in. That is deliberately blunt, and it has consequences worth knowing before you read the output.
The file the definition lives in is read differently, because that file
has already been parsed to find the definition in the first place. There
a use has to be code that reads the name: a call, a decorator, a default,
a base class, an annotation, or a parameter of that name, which is how a
fixture is asked for. A function its own file calls is a helper doing its
job, and reporting it would be wrong. The same word inside a docstring, a
comment or an __all__ entry is prose about the definition rather than a
use of it, and crediting that would leave a dead definition unreported. A
def or class statement binds its name without reading it, so nothing
counts as its own use.
Matching on a bare name in every other file means a definition reads as used when any other file happens to contain that word, including a file that defines its own unrelated function of the same name. The count is a lower bound rather than an exact figure.
Two dead functions in one file that call each other still read as used, because those calls are real code. Finding those needs reachability from a live entry point, which is a different tool.
Only top-level def and class statements are read. A method and a
nested function are reached through the name of the thing that holds
them, so neither is something this tool can speak about. Names starting
with an underscore are skipped.
Definitions a runtime invokes
A use has to be written down somewhere for the search to find it, and
some definitions are never written down. pytest finds a test by scanning
a directory and matching a prefix, so a test function's name appears
nowhere but its own def line. The same goes for a pytest_ hook the
plugin manager calls, for a fixture asked for under the name in its
decorator, and for a Flask view or a Lambda handler reached from outside
Python. Pointed at a test tree, the tool reports every test in it.
Two options let you name those definitions so the tool leaves them alone. Both default to empty, so a run passing neither answers exactly as it did before.
--assume-used-matching takes globs matched against a definition's name.
--assume-used-decorated-with takes dotted paths matched against the
decorators a definition carries, so pytest.fixture matches
@pytest.fixture and @pytest.fixture(name="x") alike. A definition
either one matches is neither searched for nor reported, and --verbose
says how many each took out, so a pattern matching more than you meant is
visible rather than silent.
Neither option knows what pytest is. A repository running pytest says so itself:
assert-python-definition-is-used test \
--search-in test \
--assume-used-matching 'test_*,Test*,pytest_*' \
--assume-used-decorated-with pytest.fixture
One running Flask passes --assume-used-decorated-with app.route, one
running Celery passes task, and one deploying to Lambda passes
--assume-used-matching lambda_handler.
Packages and their own tests
The usual thing to leave out is a package's own tests, and
--dont-search-in takes a path template rather than a fixed layout,
because the convention differs between repositories. The {package}
field is the first directory below the definition tree:
lib/python/aws_clients/__init__.py -> package is "aws_clients"
So --dont-search-in 'test/lib/python/test_{package}' leaves out
test/lib/python/test_aws_clients/, and
--dont-search-in 'test/lib/python/{package}' leaves out
test/lib/python/aws_clients/.
A file sitting directly in the definition tree belongs to no package and so has no directory of its own to leave out. Uses of its definitions count wherever they appear.
--dont-search-in is not --exclude. An excluded file is dropped from
the run altogether, so its own definitions go unchecked too. A file left
out of the search is still read for the definitions it holds; it just
does not get a vote on whether anything else is used.
GitHub Actions
- name: Assert every definition is used outside its own tests
uses: 10U-Labs/assert-python-definition-is-used@latest
with:
dont-search-in: test/lib/python/test_{package}
search-in: lib/python scripts src test
trees: lib/python
verbose: true
Over a test tree, name what pytest invokes:
- name: Assert every test helper is used
uses: 10U-Labs/assert-python-definition-is-used@latest
with:
assume-used-decorated-with: pytest.fixture
assume-used-matching: test_*,Test*,pytest_*
search-in: test
trees: test
verbose: true
License
Apache-2.0
Metadata
Release files for assert-python-definition-is-used 20260828104252
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| assert_python_definition_is_used-20260828104252.tar.gz | 41.4 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| assert_python_definition_is_used-20260828104252-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 58.9 kB
Release files / assert_python_definition_is_used-20260828104252.tar.gz
| Download URL | assert_python_definition_is_used-20260828104252.tar.gz |
|---|---|
| Size | 41.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ea5bdd49dcd1591c785e0c079ed2203c8f82aea582074e75e2633acc12b2523a
|
|
BLAKE2b-256 checksum How to use checksums |
7b0d2511382129f38a371d40768128571462e4b7cfb62eb96997958e9fe75278
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 28, 2026.
Transparency logRelease files / assert_python_definition_is_used-20260828104252-py3-none-any.whl
| Download URL | assert_python_definition_is_used-20260828104252-py3-none-any.whl |
|---|---|
| Size | 17.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
eb6bf54d0122907ec047390dfa17f9abf41cf7850178e7f8500c47bb8e6a700a
|
|
BLAKE2b-256 checksum How to use checksums |
dbcd76ceeb6da039496a2db41a3c9030a5be84e4d8cba634cd5a58f19febd783
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 28, 2026.
Transparency logSecurity for assert-python-definition-is-used 20260828104252
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 2 files in this release have been uploaded with provenance metadata.
Attestation for assert_python_definition_is_used-20260828104252.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 Aug 28, 2026.
Transparency logA 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.