Languages / Python

Python static analysis: dead code, complexity and call graphs

To find dead code in a Python project, run Vulture for unused names, or run repowise dead-code to get unreachable files and unused exports from a resolved import and call graph. repowise knows Django URLconfs, FastAPI routers, Flask blueprints and pytest fixtures, so framework entry points are not reported, and each finding carries a 0 to 1 confidence.

Last reviewed: November 2026

Full tier

What repowise detects in Python

Checked against the engine source. Every row runs locally with zero LLM calls.

repowise capabilities for Python
Files parsed.py and .pyi
Import resolutionFull. Source-root aware: src/ layouts, monorepo packages/*/src, PEP 420 namespace packages, relative imports and __init__ re-exports
Call graphResolved calls with a confidence on every edge, plus inheritance
Dead codeUnreachable files, unused exports, unused internals; deletion-ready flag on unused exports
Health markersComplexity, class metrics, Extract Method plans, assertion smells, mock saturation, assertion-free tests
Performance markersI/O in loops, blocking calls in async def, serial await in loops, pandas iterrows and pd.concat in loops
Coverage reportsCobertura XML from coverage.py, lcov, and the coverage.py .coverage database with per-test contexts
Frameworks recognisedDjango (urls to views, admin, forms, serializers), FastAPI include_router, Flask register_blueprint, pytest conftest and fixtures, Alembic

Specific to this language

How Python code is read

Framework entry points are not dead code

A view function nothing imports is still live when urls.py names it. repowise links urls.py to the view modules and sub-URLconfs it includes, FastAPI apps to their routers and Flask apps to their blueprints. Decorator-registered code is exempt before scoring: Celery task decorators (app.task, shared_task), admin.register, signal receivers, template tags, FastAPI exception handlers and middleware, click and typer commands, and context managers.

pytest fixtures are edges, not orphans

A fixture defined in conftest.py is used by name, never imported. repowise turns fixture requests into edges: test parameters, fixture parameters, usefixtures, pytestmark and getfixturevalue. A fixture nothing requests can still be reported; one that a test asks for by name is not.

Dynamic imports lower confidence

Code loaded through importlib, import_module, __import__, import_string, entry_points, pkgutil or pkg_resources lowers the confidence of nearby files, so a plugin directory read at runtime lands in the review tier and not in the deletion-ready list.

Honest comparison

Native Python tools, and when they are enough

Native Python analysis tools
ToolWhat it covers
VultureUnused functions, classes, variables and imports, each with a confidence percentage. Single files or whole packages. No framework model.
RuffUnused imports (F401) and variables (F841) as lint rules, McCabe complexity (C901). Very fast, file by file.
radonCyclomatic complexity, Halstead metrics and a maintainability index per function and file.
deptryUnused, missing and misplaced dependencies against pyproject.toml.
coverage.pyLine and branch coverage. Writes the XML and database that repowise reads.

Where repowise adds value

  • Framework awareness: Django, FastAPI, Flask and pytest wiring is in the graph, so the report is not a list of views and fixtures to whitelist.
  • File-level and package-level findings (unreachable modules, zombie packages), not only unused names.
  • Git history on every finding: a module untouched for a year starts at 1.0 confidence, one edited this month at 0.4, and each row names the file's primary owner.
  • The Python service, its TypeScript frontend and its SQL models sit in one graph, so one command covers the repository.

How to run it

Analyse a Python repository

From the repository root. Agents get the same results through the get_dead_code, get_health and get_context MCP tools.

uv tool install repowise        # or: pipx install repowise
repowise init --yes --no-prose     # index the repo, no API key
repowise dead-code --min-confidence 0.7 --safe-only
repowise health --refactoring-targets
pytest --cov --cov-report=xml
repowise coverage add coverage.xml
repowise serve                     # graph, health and dead code in a local dashboard

Questions

What people ask before they try it.

Is repowise a Vulture alternative?

It overlaps on unused functions and classes and goes further on files and packages. Vulture reads names in the code you point it at. repowise resolves imports and calls across the whole repository, knows Django, FastAPI, Flask and pytest entry points, and starts each finding's confidence from git history. Many teams keep Vulture in pre-commit and use repowise for repository-wide cleanup.

How do I generate a call graph for a Python project?

Run repowise init in the repository. It parses every .py file, resolves imports including src layouts and namespace packages, and records each call edge with one of 39 resolution origins and a fixed confidence. Browse it with repowise serve, or ask an agent through the get_context and get_answer MCP tools.

How do I measure cyclomatic complexity in Python?

radon cc gives per-function cyclomatic complexity. repowise computes complexity as one input to a 1.0 to 10.0 file health score, alongside class size, test smells, performance markers, coverage and how often the file changes, and it proposes Extract Method plans for the functions that drive the score.

Will it flag my Django management commands and migrations?

Migrations, settings and management commands are loaded by Django, not imported, so they are treated as entry points or kept at the review tier. The deletion-ready flag is held to a strict bar: unused exports on paths with no runtime-load risk, so agents can delete them safely.

Does repowise send my Python code to an LLM?

No. Parsing, the call graph, dead code, health scores, performance markers and git history all run locally with zero LLM calls. repowise init --yes --no-prose needs no API key, and the analysis itself makes no network calls. Written documentation prose is the one opt-in step that uses a model, and it can run on a local Ollama model.

Index a Python repository.

Public repositories are free on repowise.dev. The CLI is open source and runs on your machine with no API key.