Skip to main content

deadcode-hunter

Find dead code in Python and Java projects — and prove it's safe to delete.

build python license version

Most dead-code detectors (like vulture) do text-level "did this name appear anywhere" matching — cross-file imports, instance methods and decorators quickly turn into false positives or misses. deadcode-hunter does cross-file reference resolution: it understands import chains, self method calls, and method calls after obj = Klass(), then grades every candidate with a confidence level — only "deleting this is definitely safe" gets marked high.

Zero heavy dependencies (CLI is just typer + rich), the analysis engine uses only stdlib ast. No network, no LLM calls, fully deterministic.

Install

pip install deadcode-hunter
# with Java support (tree-sitter based scanner)
pip install "deadcode-hunter[java]"
# or from source
pip install -e ".[dev]"

Requires Python >= 3.10.

Usage

scan — find candidates

$ deadcode-hunter scan ./src
               deadcode-hunter: 6 candidate(s) in tests/fixtures/sample_repo/dead.py
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ File    ┃ Symbol                     ┃ Kind     ┃ Confidence ┃ Evidence                          ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ dead.py │ dead.os                    │ import   │ high       │ imported but never used           │
│ dead.py │ dead.system                │ import   │ high       │ imported but never used           │
│ dead.py │ dead.DEAD_CONST            │ variable │ high       │ no references found in the        │
│         │                            │          │            │ scanned codebase                  │
│ dead.py │ dead.dead_func             │ function │ high       │ no references found in the        │
│         │                            │          │            │ scanned codebase                  │
│ dead.py │ dead.DeadClass             │ class    │ high       │ no references found in the        │
│         │                            │          │            │ scanned codebase                  │
│ dead.py │ dead.DeadClass.dead_method │ method   │ high       │ no references found in the        │
│         │                            │          │            │ scanned codebase                  │
└─────────┴────────────────────────────┴──────────┴────────────┴───────────────────────────────────┘
high: 6  medium: 0  low: 0
# only "safe to delete"
deadcode-hunter scan ./src --min-confidence high

# machine-readable, for CI / scripts
deadcode-hunter scan ./src --format json --min-confidence high

prove — deletion safety proof

After scan finds candidates, prove dynamically verifies each one: delete it, run your test suite, and only a green suite counts as "proven safe".

$ deadcode-hunter prove ./src
[1/3] proving mymod.dead_func ...
[2/3] proving mymod.DeadClass ...
[3/3] proving mymod.DeadClass.run ...
    deadcode-hunter prove: /tmp/demo  (python -m pytest -x -q)
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓
┃ File     ┃ Symbol              ┃ Result ┃ Detail                ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩
│ mymod.py │ mymod.dead_func     │ proven │ tests pass without it │
│ mymod.py │ mymod.DeadClass     │ proven │ tests pass without it │
│ mymod.py │ mymod.DeadClass.run │ proven │ tests pass without it │
└──────────┴─────────────────────┴────────┴───────────────────────┘
proven: 3  breaks: 0  skipped: 0
# prove medium-and-up candidates
deadcode-hunter prove ./src --min-confidence medium

# point at a faster test subset
deadcode-hunter prove ./src --test-cmd "pytest tests/test_utils.py -q"

# machine-readable
deadcode-hunter prove ./src --format json

How it works, in one sentence: isolated git worktrees + your test suite as the deletion safety proof — every deletion happens in a clean checkout of HEAD, your working tree is never touched; green suite → proven, red suite → breaks (with a failure summary), too-complex-to-delete → skipped with a reason.

Deletion is AST-level: precise lineno/end_lineno removal (decorator lines included); from x import a, b only drops the dead name; hollowing out a class body auto-inserts pass before verifying — the same as deleting by hand.

Performance note: each candidate runs the full test suite once, which is slow on large repos. Start small, or use --test-cmd to target a test subset.

report — visual HTML report

A single self-contained HTML file: zero external dependencies (all CSS/JS inlined, no CDN), double-click to open in a browser.

$ deadcode-hunter report ./src --output /tmp/deadcode.html
Report written to /tmp/demo-report.html (3 candidates)
# merge prove results into the report (proven / breaks badges + pass counts)
deadcode-hunter prove ./src --format json > prove.json
deadcode-hunter report ./src --prove-json prove.json

The report includes:

  • Summary header: total candidates, confidence distribution, per-file breakdown, prove pass counts (if merged)
  • Clickable file tree on the left, symbol list on the right: name, kind, color-coded confidence badges, evidence chain
  • Click a symbol to expand the source snippet with dead lines highlighted
  • Search box to filter by symbol name
  • Dark mode follows prefers-color-scheme

clean — one-command cleanup

prove tells you a deletion is safe; clean finishes the job: it scans, proves every candidate in an isolated git worktree, and applies the proven-safe deletions to your working tree. Changes are left uncommitted — review with git diff and commit when happy.

$ deadcode-hunter clean . --dry-run
 deadcode-hunter clean: 3 proven-safe deletion(s) in .
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ File       ┃ Symbol                      ┃ Kind     ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ billing.py │ billing.legacy_discount     │ function │
│ billing.py │ billing.LegacyReport        │ class    │
│ billing.py │ billing.LegacyReport.render │ method   │
└────────────┴─────────────────────────────┴──────────┘
Dry run: no files were changed.

Safety rails: the target must be a git repo with a clean working tree (commit or stash first — clean refuses otherwise), deletions reuse the same AST-driven logic as prove, and you confirm the list unless you pass --yes.

deadcode-hunter clean . --yes            # apply without asking
deadcode-hunter clean . -i               # pick deletions one by one (y/n/a/q)
deadcode-hunter clean . --format json    # machine-readable plan + result

With --interactive (-i), each proven candidate is shown with a source snippet and you decide per item: y delete, n skip, a delete this and all remaining, q abort (nothing is applied). Needs a terminal — scripts and CI should use --yes instead.

Baseline mode — only new dead code

One-off cleanups are nice; never adding new dead code is nicer. Save a baseline once and commit it, then gate CI on it:

$ deadcode-hunter scan . --save-baseline baseline.json --min-confidence high
Baseline saved to baseline.json (3 candidates)
$ deadcode-hunter scan . --baseline baseline.json --min-confidence high
                      deadcode-hunter: 1 candidate(s) in .
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
┃ File       ┃ Symbol             ┃ Kind     ┃ Confidence ┃ Evidence           ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
│ billing.py │ billing.another_l… │ function │ high       │ no references      │
│            │                    │          │            │ found in the       │
│            │                    │          │            │ scanned codebase   │
└────────────┴────────────────────┴──────────┴────────────┴────────────────────┘
high: 1  medium: 0  low: 0

Only candidates absent from the baseline are reported. --fail-on-new exits 1 when anything (new) is found — the CI gate:

deadcode-hunter scan src --min-confidence high --fail-on-new

Configuration file

Put project defaults in the scanned project's own pyproject.toml (only that file is read — never parent directories):

[tool.deadcode-hunter]
exclude = ["tests/fixtures/**", "docs/**"]  # glob on the scan-root-relative path
entry-points = ["mypackage.api.*"]          # glob on the symbol qualname: never reported
min-confidence = "high"                     # default for scan / prove / clean
test-cmd = "pytest -q"                      # default test command for prove / clean

CLI flags always override config values.

deps — unused dependencies

A different axis of dead weight: packages declared in requirements.txt or [project] dependencies that nothing ever imports. Import names are mapped to distribution names (a built-in table covers the famous mismatches — yaml → PyYAML, PIL → pillow, sklearn → scikit-learn — the rest is case/-/_ normalized).

$ deadcode-hunter deps .
        deadcode-hunter deps: unused dependencies in .
┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ Package         ┃ Declared in      ┃ Requirement          ┃
┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ some-unused-pkg │ requirements.txt │ some-unused-pkg==1.0 │
└─────────────────┴──────────────────┴──────────────────────┘
Skipped lines:
  line 7: -r nested.txt (not supported)
used: 4  unused: 1  undetermined: 0

Honesty rule: when the project uses dynamic imports (__import__, getattr, …), an "unused" verdict can't be trusted — those go into a separate undetermined list instead of being declared dead. -r includes and URL requirements are reported as skipped, not crashed on.

duplicates — copy-pasted code

Dead code's cousin. Lines are token-normalized (identifiers → ID, strings/numbers → LIT, keywords kept), then a sliding window finds blocks that match even after renaming:

$ deadcode-hunter duplicates .
Group 1: 11 lines × 2 occurrence(s), 56 tokens
┏━━━━━━┳━━━━━━━┓
┃ File ┃ Lines ┃
┡━━━━━━╇━━━━━━━┩
│ a.py │ 1-11  │
│ b.py │ 1-11  │
└──────┴───────┘
deadcode-hunter duplicates . --min-lines 6 --min-tokens 50  # defaults
deadcode-hunter duplicates . --format json                  # machine-readable

Same-file and cross-file duplicates are both reported, grouped by block. Python only in v1 (TypeScript/Java are roadmap).

Confidence levels

Level Meaning Example
high (deletable) Zero references in the whole codebase, no reason to be alive A function never imported, never called
medium (take a look) Weak reference evidence, or decorated A @app.route view, an unresolvable obj.attr access
low (probably alive) Its scope uses getattr/eval/__import__ Plugin-style dynamically loaded code

Every candidate carries an evidence chain: definition location, reference counts, and why it was judged dead/suspicious.

What it gets right

  • Symbols exported via __all__ are not reported (that's public API)
  • Definitions inside if __name__ == "__main__" blocks are not reported
  • Dunder methods (__str__, …) and @abstractmethod are not reported
  • Decorated functions → downgraded to medium (the decorator may be a framework registration)
  • getattr / eval / __import__ / string-concatenated imports → enclosing scope downgraded to low
  • Test files (test_*.py, tests/ dirs) are analyzed normally, but test_* functions and @pytest.fixtures are not reported (and marked as test origin)
  • Cross-file: from pkg.mod import foo, import pkg.mod + pkg.mod.foo() are chained
  • Instance methods: self._helper(), worker = Worker(); worker.run(), Worker().run() all resolve
  • Annotations as types: x.bar() inside def f(x: Foo) resolves to Foo.bar
  • Respects .gitignore; skips .venv / __pycache__ / .git

Java support

The Java engine is built on tree-sitter and feeds the same pipeline — same confidence levels, evidence chains, prove, and HTML reports. Language is detected by file extension (.py → Python, .java → Java); directories mixing both scan both. Override with --lang:

deadcode-hunter scan ./my-java-project
deadcode-hunter scan ./mixed --lang java   # only .java files
deadcode-hunter scan ./mixed --lang python # only .py files

Java-specific rules:

  • main methods (and the class containing one) are entry points — never reported
  • @Override methods are treated as alive by construction (wired to their supertype)
  • Other annotations (e.g. @Test) downgrade to medium, like Python decorators
  • private members with no references → high; public ones → high with a "may be a public API" note in the evidence chain
  • Reflection string literals (Class.forName("com.foo.Bar"), getMethod("name")) downgrade their targets to medium instead of high
  • Unused imports → high; import pkg.* is never flagged (the names it provides are unknowable, so flagging it would be a false positive)
  • Constructors are tracked: new Foo() keeps both the class and its constructor alive

For prove on Java projects, point --test-cmd at your own build:

deadcode-hunter prove ./my-java-project --test-cmd "mvn -q test"
# or: deadcode-hunter prove ./my-java-project --test-cmd "gradle test"

Java-specific limitations: no full type inference (same conservative weak-reference rule as Python); annotation processors and DI frameworks (Spring, Dagger, …) wire code invisibly — treat high on framework-managed beans with suspicion.

TypeScript support

The TypeScript engine is built on tree-sitter (tree-sitter-typescript + tree-sitter-javascript) and feeds the same pipeline — same confidence levels, evidence chains, prove, and HTML reports. .ts uses the typescript grammar, .tsx the tsx grammar (including JSX component references), .js/.jsx the javascript grammar. Language is detected by file extension; directories mixing Python, Java and TypeScript scan all three. Override with --lang:

pip install "deadcode-hunter[typescript]"

deadcode-hunter scan ./my-ts-project
deadcode-hunter scan ./mixed --lang typescript   # only .ts/.tsx/.js/.jsx files

TypeScript-specific rules:

  • export-ed symbols are the module's public surface and are never reported (like Python's __all__) — this includes members of exported classes and names from export { a, b }
  • Interfaces and type aliases are contracts — never reported; enums are real values and are analyzed
  • Decorated classes/functions → medium (members of a decorated class inherit the downgrade)
  • this.x resolves to the enclosing class's members; new Foo() keeps the class alive; const s = new Service() + s.serve() resolves via initializer type tracking
  • console/process/window/globalThis member accesses are platform globals, not references — no noise
  • Relative imports (./util, ../lib) resolve to files on disk; side-effect imports (import "./polyfill") are never flagged

For prove/clean on TypeScript projects, point --test-cmd at your own runner:

deadcode-hunter prove ./my-ts-project --test-cmd "npm test"
deadcode-hunter clean ./my-ts-project --test-cmd "npx jest"

TypeScript-specific limitations: same conservative weak-reference rule as Python/Java for untyped variables; framework magic (Angular DI, decorators that register routes) is invisible — treat high on framework-managed code with suspicion.

GitHub Action

Use deadcode-hunter in CI with the composite action (action.yml):

- uses: zheng0629/deadcode-hunter@v0.4.0
  with:
    path: .
    min-confidence: high
Input Default Description
path . Directory (or file) to scan
min-confidence high Minimum confidence: high, medium or low
pip-version 0.4.0 deadcode-hunter version to install from PyPI

The action exposes a findings output (total=3 high=2 medium=1 low=0). See examples/pr-comment.yml for a copy-paste workflow that comments the findings table on every pull request (creates the comment once, then updates it in place).

This repo dogfoods it: .github/workflows/ci.yml runs the test suite and then scan src --min-confidence high --fail-on-new as a gate.

deadcode-hunter vs vulture

vulture deadcode-hunter
Reference analysis Name matching within a single file Cross-file import chains + reference resolution
Instance methods Barely supported (self._helper false-positives) self / variable types / constructor chains resolve
Output Dead-code list Dead-code list + confidence levels + evidence chain
Safety proof — prove: delete + run your test suite in an isolated git worktree
Report Text list Self-contained HTML report (file tree, evidence, source snippets, dark mode)
Dynamic code Easily misjudged getattr/eval scopes auto-downgraded to low
Determinism Deterministic Deterministic (no LLM, no network)

In short: vulture answers "this code is probably unused"; deadcode-hunter answers "deleting this code is safe, and here's the evidence".

Limitations

Honest ones, so you can trust the rest:

  • It only sees inside the scanned codebase. A library's public API with no internal callers is reported high — that's an honest "no internal references". Check for external callers before deleting.
  • prove trusts your test suite. It proves "the suite stays green after deletion" — nothing more. Behavior your tests don't cover is invisible to it.
  • No full type inference. x.attr where x is an unannotated local or loop variable is recorded as a weak reference (medium) — a deliberate conservative choice.
  • It doesn't execute code. __getattr__, metaclass magic, and calls inside C extensions are invisible.

Roadmap

  • ✅ M1 — cross-file scanner (done): reference resolution across files, high/medium/low confidence with evidence chains.
  • ✅ M2 — prove (done): deletion safety proof via isolated git worktrees + your test suite.
  • ✅ M3 — HTML report (done): single self-contained file, file tree, evidence, source snippets, dark mode, --prove-json merge.
  • ✅ Java support (done): tree-sitter based scanner (optional pip install "deadcode-hunter[java]"), same pipeline — confidence levels, evidence chains, prove, HTML reports; --lang auto|python|java to select.
  • ✅ M6 — clean (done): scan → prove → apply proven-safe deletions to the working tree (--dry-run preview, --yes to skip confirmation, dirty tree refused).
  • ✅ M7 — GitHub Action (done): composite action.yml + copy-paste PR-comment workflow example + this repo's own CI with a --fail-on-new dogfood gate.
  • ✅ M8 — baseline mode (done): --save-baseline / --baseline report only new dead code; --fail-on-new exits 1 for CI.
  • ✅ M9 — config file (done): [tool.deadcode-hunter] in the scanned project's pyproject.toml — exclude, entry-points, min-confidence, test-cmd defaults.
  • ✅ TypeScript support (done): tree-sitter based scanner (optional pip install "deadcode-hunter[typescript]") for .ts/.tsx/.js/.jsx — same pipeline, exports treated as public API (never reported), JSX component references, --lang typescript.
  • ✅ M11 — deps (done): declared-but-unused dependency detection from requirements.txt / [project] dependencies, with an import→distribution mapping table and an honest undetermined bucket when dynamic imports are in play.
  • ✅ M12 — duplicates (done): token-normalized copy-paste detection (Python), same-file and cross-file, grouped output, --min-lines / --min-tokens tunables.
  • ✅ M13 — interactive clean (done): clean --interactive (-i) picks proven deletions one by one (y/n/a/q) with source snippets; needs a terminal, refused otherwise.
  • LLM-assisted investigator: an optional LLM pass over low-confidence candidates to suggest why something might still be alive. The static engine stays deterministic — the LLM only advises, never decides.

Contributing

python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
python -m pytest tests/ -q

PRs are welcome. Two house rules:

  1. Keep the analysis engine deterministic — no network calls, no LLM in the core pipeline.
  2. Add a fixture case in tests/fixtures/sample_repo/ for every new detection rule, plus a test.

License

MIT — see LICENSE.


中文文档:README.zh-CN.md

Metadata

Release files for deadcode-hunter 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for deadcode-hunter 0.4.0
File Size Uploaded Provenance Actions
deadcode_hunter-0.4.0.tar.gz 80.8 kB Without provenance Details for deadcode_hunter-0.4.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for deadcode-hunter 0.4.0
File Interpreter ABI Platform Actions
deadcode_hunter-0.4.0-py3-none-any.whl Python 3 none any Details for deadcode_hunter-0.4.0-py3-none-any.whl

Total release size: 148.9 kB

Release files / deadcode_hunter-0.4.0.tar.gz

Download URL deadcode_hunter-0.4.0.tar.gz
Size 80.8 kB
Tags Source
SHA-256 checksum
How to use checksums
ea3e478c9e113bcd7365d46de05cb085f8420c2121fe1db2316c09431775ae98
BLAKE2b-256 checksum
How to use checksums
6215342089072d240a92c2f3aa764c3e37cd53122cc45d7650cb291dbf225e0f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / deadcode_hunter-0.4.0-py3-none-any.whl

Download URL deadcode_hunter-0.4.0-py3-none-any.whl
Size 68.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d0486afc2e4bba22f38cdd088049ce34f9d49c49d66febe5422172ebd00799ec
BLAKE2b-256 checksum
How to use checksums
a2ace7c912d22f0d7e15f99a672344aef87b860b5dce4895cbc2d6e0745df265
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Security for deadcode-hunter 0.4.0

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

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.

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files No provenance

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