Skip to main content

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.

  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.

# 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

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

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           # 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

dead_code_detector-0.4.1.tar.gz (15.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dead_code_detector-0.4.1-py3-none-any.whl (19.9 kB view details)

Uploaded Python 3

File details

Details for the file dead_code_detector-0.4.1.tar.gz.

File metadata

  • Download URL: dead_code_detector-0.4.1.tar.gz
  • Upload date:
  • Size: 15.6 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

Hashes for dead_code_detector-0.4.1.tar.gz
Algorithm Hash digest
SHA256 574872edad8dc5ef5056681c38ed553f3ed417f3d015213e71ced966e5a23f52
MD5 64b4bebd37689da0eca1e4a8add3acf7
BLAKE2b-256 9f5962a7480c68f50374e086289a90f3db791addcd62237cd5af916a18ae644b

See more details on using hashes here.

File details

Details for the file dead_code_detector-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: dead_code_detector-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 19.9 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

Hashes for dead_code_detector-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 dc26c376faae5e9c490f59ab5d5f52ee58ca5d7d27bd2fb7a2960545077adb8d
MD5 edb9810ed0c3a9539b0d8d57da4cb238
BLAKE2b-256 5f993d2b9c4857ccc4a2a088de6bcc6ca23a5fa8388e6b10c52a83d4ef10ce03

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page