A fast, Rust-based dead code detector for Python, built on tree-sitter.
deadpy scans a Python codebase, parses every file in parallel (via rayon), and reports functions, classes, methods, variables, and imports that are defined but never referenced anywhere else in the project.
Static "find unused code" tools for Python (e.g. vulture) are invaluable in CI but can be slow on large codebases, since they re-parse and re-walk the AST with the Python interpreter itself. deadpy uses tree-sitter to parse everything up front and in parallel, then resolves references in a single in-memory pass — no Python runtime involved.
Each release ships prebuilt binaries for Linux (x86_64/aarch64) and Apple Silicon macOS. Grab one directly, or install via cargo-binstall (downloads the prebuilt binary, no compile):
cargo binstall deadpyCompiles from source; requires a Rust toolchain:
cargo install deadpygit clone https://github.com/brightwave-inc/deadpy.git
cd deadpy
cargo build --release
./target/release/deadpy --helpComing soon:
pip install deadpy(PyPI wheels, sopip/uvx/pipxcan install it with no Rust toolchain).
# Scan the current directory (or the paths from pyproject.toml / CLI args)
deadpy
# Scan specific paths
deadpy src/ scripts/
# Only report unused functions and classes
deadpy --report function,class
# Machine-readable output
deadpy --format json
deadpy --format count # just the number of findings, for CI gatingdeadpy exits 1 if any dead code is found and 0 otherwise, so it drops straight into CI.
| Flag | Description |
|---|---|
[paths]... |
Paths to scan (overrides config) |
--config <PATH> |
Path to pyproject.toml (default: ./pyproject.toml) |
--exclude <PATTERN> |
Additional exclude pattern (repeatable) |
--ignore-decorators <PATTERN> |
Additional decorator pattern to ignore (repeatable) |
--ignore-names <PATTERN> |
Additional name glob to ignore (repeatable) |
--min-confidence <N> |
Minimum confidence (0-100) to report, default 60 |
--whitelist <FILE> |
Additional whitelist file (repeatable) |
--report <KINDS> |
Comma-separated kinds to report: function,class,method,variable,import |
--sort-by-size |
Sort findings by code size (largest first) instead of file:line |
--format <FMT> |
Output format: text (default), json, count |
-q, --quiet |
Only print findings, no scan summary |
All of the above (except --config itself) can be set under [tool.deadpy] in your pyproject.toml. CLI flags extend or override the config:
[tool.deadpy]
paths = ["src/"]
exclude = [".venv/", "*/migrations/*"]
ignore_names = ["_legacy_*"]
ignore_decorators = ["@app.route", "@celery.task"]
min_confidence = 70
report = ["function", "class", "method"]
whitelist = ["whitelist.py"]deadpy exits non-zero when it finds dead code, so it gates a build with no extra scripting. Install the prebuilt binary with cargo-binstall (no compilation) and run it:
name: dead-code
on: [push, pull_request]
jobs:
deadpy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cargo-bins/cargo-binstall@main
# Pin a version for reproducible CI (drop @x.y.z to always use latest).
- run: cargo binstall -y deadpy@0.1.3
- run: deadpyThe final deadpy step fails the job if any dead code is found. It reads [tool.deadpy] from your pyproject.toml (or pass paths/flags directly, e.g. deadpy src/ --min-confidence 80).
deadpy:
image: rust:latest
script:
- cargo install cargo-binstall
- cargo binstall -y deadpy@0.1.3
- deadpyFor a machine-readable gate (e.g. to post a count), use deadpy --format json or deadpy --format count.
Add deadpy to your pre-commit config:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/brightwave-inc/deadpy
rev: v0.1.3
hooks:
- id: deadpyThe hook scans your whole project on every run — not just the changed files, since dead-code analysis needs the full codebase to know whether a definition is referenced anywhere — and honors [tool.deadpy] in your pyproject.toml.
Requires a Rust toolchain on the machine running the hook (pre-commit builds deadpy from source). A zero-toolchain hook will follow once prebuilt wheels ship on PyPI.
A definition (function, class, method, variable, or import) is flagged unless one of the following holds:
- Its name — or, for methods, its qualified
Class.methodname — appears as a reference anywhere else in the scanned code: a call, attribute access, import, or bare identifier. - It's a Python dunder method (
__init__,__str__,__enter__, …). These are part of the data model and invoked implicitly by the interpreter, so they're always excluded. - It matches an
ignore_namesglob (e.g."test_*"), or carries a decorator matching anignore_decoratorspattern (e.g."@app.route"— useful for framework-registered handlers never called directly in Python). - It's referenced from a whitelist file — a
.pyfile of dummy references (e.g.MyClass.some_method) documenting known-but-indirect usages (dynamic dispatch, string-based lookup, etc.), the same escape hatch vulture uses. - Names listed only in
__all__are not treated as references — re-export alone doesn't prove a symbol is consumed, so a name that appears in__all__but is otherwise unused is still flagged.
Every finding carries a confidence score, gated by --min-confidence. Today all findings score a flat 60; the field exists so future heuristics (e.g. lower confidence for __all__-only exports or dynamically dispatched names) can be layered in without changing the config surface.
cargo test
cargo fmt --check
cargo clippy --all-targets -- -D warningsMIT — see LICENSE.