Skip to main content

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.

  1. Import the entry point modules. All transitively imported modules are discovered via sys.modules.
  2. Index every function, method, and class using inspect, with an AST fallback for decorated objects (e.g. Click commands).
  3. Parse every discovered module's AST to extract references: imports, name usage, attribute access, type annotations, base classes.
  4. 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. Entry points can also be set in pyproject.toml (see Configuration below).

# 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

Report types

Select the report type with --report (default: flat).

--report flat (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()

--report verbose (or -v) — adds line counts and cross-references between dead items.

--report 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%

--report 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.

--report 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
--report TYPE Report type: flat, verbose, stats, usage, json
-v Shorthand for --report verbose
--exclude PATTERN Exclude modules matching pattern (repeatable)
--live-decorators Treat all decorated definitions as live
--include-dunders Report unreachable dunder methods
-o, --output FILE Write to file instead of stdout
--exit-code Exit with code 1 if dead code is found

Configuration

Project-level defaults can be set in pyproject.toml so you don't have to repeat options on every invocation:

[tool.dead-code-detector]
entry-points = ["myapp.cli", "myapp.worker"]
src-dir = "src"
report = "flat"
exclude = ["migrations", "conftest"]
live-decorators = true
exit-code = true

With this config, you can just run dead-code-detector with no arguments.

CLI arguments always override config values. For lists (entry-points, exclude), CLI values replace the config (not append).

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 to SomeClass
  • Decorators and base classes: decorator expressions and base class names are tracked as references
  • Private methods: _underscore_prefixed names 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 --exclude or specify them as entry points.
  • TYPE_CHECKING imports: modules imported only inside if 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           # CLI (argparse + pyproject.toml config)
    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

Metadata

Release files for dead-code-detector 0.5.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 dead-code-detector 0.5.0
File Size Uploaded
dead_code_detector-0.5.0.tar.gz 16.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dead-code-detector 0.5.0
File Interpreter ABI Platform
dead_code_detector-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 37.7 kB

Release files / dead_code_detector-0.5.0.tar.gz

Download URL dead_code_detector-0.5.0.tar.gz
Size 16.7 kB
Tags Source
SHA-256 checksum
How to use checksums
48f631e37c3d1fb8a04e09c3b7fd957a7fa8165b364346e3d2475522f62c3d9e
BLAKE2b-256 checksum
How to use checksums
17c9e297c28a065c2645d595ab6c5ffbd399525b7980cfa950d869c09cdff4b9
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / dead_code_detector-0.5.0-py3-none-any.whl

Download URL dead_code_detector-0.5.0-py3-none-any.whl
Size 21.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b14be0afce68e59d383084ed411ab6f30a398e51a50cc9e2c78858b9422fb2a5
BLAKE2b-256 checksum
How to use checksums
b0cca44ee1a05ca797e6f464d44ef5bc69e84247c72cb4c1e451bff49c494f1d
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

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