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

# 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 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           # 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.2.tar.gz (16.0 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.2-py3-none-any.whl (20.4 kB view details)

Uploaded Python 3

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

Hashes for dead_code_detector-0.4.2.tar.gz
Algorithm Hash digest
SHA256 bf53c6296439d4e885ab6732060c94a1663a0fd49d717cc7572f3341024b6a21
MD5 585681fe86cbdee9764fc215354cf44d
BLAKE2b-256 8e8554b3f847d389a1d13dbd48c9241bfbd64b57fc4c1fcad0b8a3451ebdba49

See more details on using hashes here.

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

Hashes for dead_code_detector-0.4.2-py3-none-any.whl
Algorithm Hash digest
SHA256 9454d8d8c458227ac594c6e247ead56fe65c26ef665ebc8b7850b35eefbe2a18
MD5 1d489792084eed1d643f936d0518aa6e
BLAKE2b-256 37f6699370dc866d99fcec403328707b968b2461701d1128d20624a3585a3826

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