Find unused functions, methods, and classes in Python codebases
Project description
dead-code-detector
Find unused functions, methods, and classes in Python codebases by walking import and call graphs from user-specified entry points.
Quick start
uv add --dev dead-code-detector
dead-code-detector --src-dir src myapp.main
How it works
The tool uses a hybrid approach: runtime import to discover definitions, AST analysis to find references, then BFS reachability from entry points.
- Import the entry point modules. All transitively imported modules are
discovered via
sys.modules. - Index every function, method, and class using
inspect, with an AST fallback for decorated objects (e.g. Click commands). - Parse every discovered module's AST to extract references: imports, name usage, attribute access, type annotations, base classes.
- Build a directed graph and run BFS from the entry points. Anything not reached is reported as dead code.
The tool overapproximates by design: when unsure whether a reference resolves to a definition, it assumes it does. This means fewer false positives (reporting live code as dead) at the cost of some false negatives (missing truly dead code).
Usage
dead-code-detector [OPTIONS] ENTRY_POINTS...
ENTRY_POINTS are one or more Python module paths. The tool imports them and
analyzes everything they transitively import.
# Single entry point
dead-code-detector --src-dir src myapp.main
# Multiple entry points
dead-code-detector --src-dir src myapp.cli myapp.worker
# Exclude modules from analysis
dead-code-detector --src-dir src --exclude migrations myapp.main
# Codebases with decorator-based registration (@register, @command, etc.)
dead-code-detector --live-decorators --src-dir src myapp.main
Output modes
Default — grouped by module, methods under their class, sorted by most dead first:
UNUSED ROUTINES (11 found):
── myapp.models (3/7 dead, 43% unused)
myapp/models.py:21 OldModel [entirely unused]
:24 .__init__()
:27 .process()
myapp/models.py User (class is used, methods dead):
:16 .legacy_export()
── myapp.utils (2/3 dead, 67% unused)
myapp/utils.py:13 helper_unused()
myapp/utils.py:18 another_unused()
Classes marked [entirely unused] have no reachable methods. When only
some methods of a class are dead, the class is shown as (class is used, methods dead).
Verbose (-v) — adds line counts and cross-references between dead
items.
Stats (--stats) — per-module coverage table sorted by most dead:
MODULE COVERAGE:
Module Definitions Reachable Dead Coverage
------------------------------------------------------------------
myapp.utils 3 1 2 33.3%
myapp.models 7 4 3 57.1%
myapp.main 2 2 0 100.0%
------------------------------------------------------------------
TOTAL 12 7 5 58.3%
Usage map (--usage) — shows every definition with its reachability
status and the shortest call path from the entry point:
USAGE MAP (7/12 reachable definitions):
── myapp.models
+ User via: myapp.main
+ .__init__() via: myapp.main -> myapp.models:User
+ .greet() via: myapp.main -> myapp.main:run
- .legacy_export()
- OldModel
- .__init__()
- .process()
+ marks reachable definitions, - marks dead ones. The via: chain shows
the shortest path through the call graph.
JSON (--format json) — machine-readable output for tooling.
CI integration
Use --exit-code to fail the build when dead code is found:
dead-code-detector --exit-code --src-dir src myapp.main
Exit codes: 0 no dead code, 1 dead code found, 2 error.
Options reference
| Option | Description |
|---|---|
--src-dir DIR |
Source directory to add to sys.path |
-v, --verbose |
Detailed output with line counts and cross-refs |
--stats |
Per-module coverage table |
--usage |
Usage map with call paths from entry points |
--format {text,json} |
Output format (default: text) |
--exclude PATTERN |
Exclude modules matching pattern (repeatable) |
--include-dunders |
Report unreachable dunder methods |
--live-decorators |
Treat all decorated definitions as live |
-o, --output FILE |
Write to file instead of stdout |
--exit-code |
Exit with code 1 if dead code is found |
What it handles
- Imports:
import x,from x import y, relative imports - Function and method calls:
foo(),obj.method(),self._helper() - Attribute access chains:
self._checker.is_ready()resolved by method name across all classes (overapproximation) - Dict/list dispatch:
HANDLERS = {"start": _handle_start}— functions stored as values are treated as referenced - Polymorphism: subclass method overrides are linked to base class methods via class hierarchy edges
- Type annotations:
def foo(x: SomeClass)counts as a reference toSomeClass - Decorators and base classes: decorator expressions and base class names are tracked as references
- Private methods:
_underscore_prefixednames are indexed and resolved - Auto-generated methods: dataclass/attrs-generated dunders are excluded (source file check)
- Click commands: decorated functions transformed into non-function objects are discovered via AST fallback
- Decorator-based registration (with
--live-decorators): classes and functions decorated with@register,@command,@hookimpl, or any other decorator are treated as live entry points. Their methods are also marked reachable.
Known limitations
- Dynamic dispatch:
getattr(obj, name)()is not traced. - String references:
globals()["func"]()is not traced. - Framework registration: Flask routes, Django views, Celery tasks are
not automatically detected. Use
--excludeor specify them as entry points. TYPE_CHECKINGimports: modules imported only insideif TYPE_CHECKING:blocks are not imported at runtime and their references are not scanned.- Lazy imports: imports inside function bodies don't execute at module import time, so lazily-imported modules may not be discovered.
- Star imports:
from module import *treats all public names as referenced (overapproximation). You shouldn't use start imports anyway.
Development
Setup
git clone https://git.sr.ht/~sfermigier/dead-code-detector
cd dead-code-detector
uv sync
Project structure
src/dead_code_detector/
cli.py # Click CLI, orchestrates the pipeline
discovery.py # Runtime import, module discovery via sys.modules
indexer.py # Definition extraction (inspect + AST fallback)
ast_analyzer.py # AST visitor for references
graph.py # Edge resolution, polymorphic edges, BFS reachability
reporter.py # Output formatting (flat, verbose, stats, usage, JSON)
models.py # Data classes (DefinitionInfo, Reference, AnalysisResult)
Pipeline
cli.py
├── discovery.import_entry_points() # import, collect sys.modules
├── indexer.index_modules() # inspect + AST fallback
├── ast_analyzer.analyze_references() # AST parse for references
├── graph.build_graph_and_analyze() # resolve edges, BFS
│ ├── _resolve_edges() # references → definition FQNs
│ ├── _add_polymorphic_edges() # base → subclass overrides
│ └── _compute_reachability() # BFS, record paths
└── reporter.report_*() # format output
Running tests
uv run pytest # all tests
uv run pytest tests/a_unit/ # unit tests only
uv run pytest tests/b_integration/ # integration + regression tests
Or just make test.
Linting and type checking
uv run ruff check src/ tests/
uv run ruff format src/ tests/
uv run ty check src
Or just make lint.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
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 dead_code_detector-0.4.2.tar.gz.
File metadata
- Download URL: dead_code_detector-0.4.2.tar.gz
- Upload date:
- Size: 16.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.1 {"installer":{"name":"uv","version":"0.11.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bf53c6296439d4e885ab6732060c94a1663a0fd49d717cc7572f3341024b6a21
|
|
| MD5 |
585681fe86cbdee9764fc215354cf44d
|
|
| BLAKE2b-256 |
8e8554b3f847d389a1d13dbd48c9241bfbd64b57fc4c1fcad0b8a3451ebdba49
|
File details
Details for the file dead_code_detector-0.4.2-py3-none-any.whl.
File metadata
- Download URL: dead_code_detector-0.4.2-py3-none-any.whl
- Upload date:
- Size: 20.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.1 {"installer":{"name":"uv","version":"0.11.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9454d8d8c458227ac594c6e247ead56fe65c26ef665ebc8b7850b35eefbe2a18
|
|
| MD5 |
1d489792084eed1d643f936d0518aa6e
|
|
| BLAKE2b-256 |
37f6699370dc866d99fcec403328707b968b2461701d1128d20624a3585a3826
|