Dead code detection for Python applications with LLM-assisted verification and removal.
Existing tools like vulture, deadcode, and flake8 don't adequately handle framework-specific patterns (Flask routes, Celery tasks).
OpenPrune:
- Auto-detects Python app types to find entrypoints (Flask routes, Celery tasks, FastAPI endpoints, CLI commands)
- Performs AST-level analysis to trace symbol usage from entrypoints
- Hands off to an LLM for verification and removal—static analysis does the heavy lifting, LLM resolves edge cases
This approach avoids flaky context engineering and token wastage from feeding entire codebases to LLMs.
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ detect │ ──▶ │ analyze │ ──▶ │ verify │ ──▶ │ delete │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │
▼ ▼ ▼ ▼
config.json results.json verified.json removals.json
-
Detection — Scans for framework patterns (Flask
@app.route, Celery@task, Click commands) and identifies entrypoints. Outputs.openprune/config.json. -
Analysis — Builds an AST-based dependency graph starting from entrypoints. Walks imports and function calls to find orphaned symbols. Scores each candidate by:
- Reference count (zero usages = high confidence)
- Import relationships (not imported elsewhere)
- Git history (files with no recent commits)
- Framework patterns (missing expected decorators)
-
Verification — Launches an LLM session with pre-built context. The LLM reviews candidates and marks each as DELETE, KEEP, or UNCERTAIN. This catches dynamic patterns that static analysis misses.
-
Deletion — Uses an LLM to remove all verified dead code. Reads
verified.json, processes each file, and removes symbols marked DELETE. Requires a clean git working tree (undo viagit checkout .).
Static analysis cannot detect:
- Dynamic imports —
importlib.import_module(),__import__() - Reflection —
getattr(obj, "method_name"),globals()["func"]() - String-based references — Template engines, ORM column names, serializers
- Plugin systems — Entry points loaded by external code
- Monkey patching — Runtime modifications to classes/modules
This is why LLM verification exists—the LLM can reason about whether a symbol might be used dynamically based on naming conventions, surrounding code, and framework patterns.
openprune detect → .openprune/config.json # Detect frameworks & entrypoints
openprune analyze → .openprune/results.json # Find dead code candidates
openprune verify → .openprune/verified.json # LLM-assisted verification
openprune delete → .openprune/removals.json # LLM-driven code removal
pip install openprune# Run full pipeline (detect + analyze)
openprune run ./my-project
# Or step by step:
openprune detect ./my-project # Generate config
openprune analyze ./my-project # Find dead code
openprune verify ./my-project # Verify with LLM
openprune delete ./my-project # Remove dead codeRun full detection and analysis in one step.
openprune run ./my-project
openprune run ./my-project --verbose # Show detailed resultsDetect frameworks and generate .openprune/config.json.
openprune detect ./my-projectDetects:
- Flask (routes, blueprints, hooks, error handlers, CLI commands)
- Celery (tasks, shared tasks, signal handlers)
- Flask-RESTPlus/Flask-RESTX (Resource classes, HTTP methods)
- SQLAlchemy (model classes, ORM decorators)
- Pydantic (BaseModel field tracking)
- pytest (test functions, fixtures)
- FastAPI, Django, Click, Typer (import-based detection)
- Main blocks (
if __name__ == "__main__") - Infrastructure files (Dockerfile, docker-compose.yml, .gitlab-ci.yml, shell scripts)
Analyze code using existing config to find dead code candidates.
openprune analyze ./my-project
openprune analyze ./my-project --verbose # Show all candidatesOutputs .openprune/results.json with confidence scores (0–100%).
Verify dead code candidates using an LLM.
# Interactive mode (default) — drops into LLM session
openprune verify ./my-project
# Auto mode — non-interactive single LLM call
openprune verify ./my-project --auto
# Select priority tiers to verify
openprune verify ./my-project --tier p0 # Default: medium confidence
openprune verify ./my-project --tier p1 --tier p2 # High confidence
openprune verify ./my-project --tier all # All tiers
# Preview what would be sent to LLM
openprune verify ./my-project --dry-run
# Include orphaned files in LLM verification (normally auto-marked DELETE)
openprune verify ./my-project --include-orphaned
# Use a different LLM CLI
openprune verify ./my-project --llm kimiPriority Tiers:
| Tier | Confidence | Type | Description |
|---|---|---|---|
| P0 | 50–79% | Non-imports | Highest LLM value (default) |
| P1 | 80–99% | Non-imports | High confidence dead code |
| P2 | 80–99% | Imports | Usually true positives |
| P3 | 100% | All | Auto-delete candidates |
| SKIP | <50% | All | Likely used, not verified |
Supported LLM CLIs:
claude(default) — Anthropic's Claude Code CLIkimi— Kimi CLIopencode— OpenCode CLI
Delete verified dead code using an LLM.
# Interactive mode (default) — drops into LLM session
openprune delete ./my-project
# Auto mode — non-interactive single LLM call
openprune delete ./my-project --auto
# Preview what would be deleted
openprune delete ./my-project --dry-run
# Skip git clean check
openprune delete ./my-project --forceRequires a clean git working tree by default. To undo all changes: git checkout .
Display results from a previous analysis.
openprune show # Show .openprune/results.json
openprune show ./path/to/results.json # Show specific file
openprune show --verbose # Detailed viewAll outputs are stored in the .openprune/ directory:
| File | Description |
|---|---|
config.json |
Detected frameworks, entrypoints, and analysis settings |
results.json |
Dead code candidates with confidence scores and reasons |
verified.json |
LLM verification results with DELETE/KEEP/UNCERTAIN verdicts |
delete_plan.json |
Compact deletion plan grouped by file (auto-generated) |
removals.json |
Deletion results tracking what was removed |
Each dead code candidate receives a confidence score based on:
- No references found — Symbol has zero usages
- No external imports — Not imported by other modules
- Framework patterns — Missing expected decorators
- File age — Old files with no recent commits
Higher confidence = more likely to be truly dead code.
╭─────────────────── Dead Code Summary ───────────────────╮
│ │
│ Total candidates 12 │
│ High confidence 5 │
│ Medium confidence 4 │
│ Low confidence 3 │
│ │
│ Estimated removable ~450 lines │
│ │
╰─────────────────────────────────────────────────────────╯
OpenPrune respects # noqa comments, which is essential for:
- Celery task registration — Importing tasks to register them with the Celery app
- Side-effect imports — Modules that perform setup when imported
- Re-exports — Public API modules that import and expose symbols
# These imports won't be flagged as dead code:
from app.tasks import send_email # noqa: F401
import celery_config # noqa
from typing import TYPE_CHECKING # type: ignoreSupported patterns:
# noqa— Suppress all checks# noqa: F401— Suppress specific code(s)# type: ignore— Type checking suppression
To disable noqa handling, set respect_noqa: false in the config.
Edit .openprune/config.json to customize:
{
"analysis": {
"include": ["**/*.py"],
"exclude": ["**/tests/**", "**/__pycache__/**"]
},
"linting": {
"respect_noqa": true,
"noqa_patterns": ["# noqa", "# type: ignore"],
"ignore_decorators": ["@pytest.fixture", "@abstractmethod", "@property"]
}
}OpenPrune uses a plugin architecture for framework detection. Built-in plugins include:
| Plugin | Detects |
|---|---|
flask |
@app.route(), @bp.route(), hooks, error handlers, CLI commands |
celery |
@app.task, @shared_task, signal handlers |
flask-restplus |
Resource subclasses, HTTP methods (get, post, etc.), api.add_resource() |
sqlalchemy |
Model classes, @validates, @hybrid_property, event listeners |
pydantic |
BaseModel fields, @field_validator, @model_validator, computed fields |
pytest |
test_* functions, Test* classes, @pytest.fixture |
OpenPrune automatically detects Flask-RESTPlus patterns:
from flask_restplus import Resource
class UserResource(Resource):
def get(self, user_id): # Detected as entrypoint
return get_user(user_id)
def put(self, user_id): # Detected as entrypoint
return update_user(user_id)
# Route registration also detected
api.add_resource(UserResource, "/users/<user_id>")HTTP methods (get, post, put, delete, patch, head, options) on Resource subclasses are automatically recognized as entrypoints and won't be flagged as dead code.
OpenPrune scans infrastructure configuration files to discover Python entrypoints that aren't visible through code analysis alone:
| File Type | Patterns Detected |
|---|---|
Dockerfile |
ENTRYPOINT, CMD, ENV FLASK_APP |
docker-compose*.yml |
command, entrypoint, environment variables |
.gitlab-ci.yml |
script sections with Python commands |
Shell scripts (.sh) |
python, gunicorn, celery, uvicorn commands |
Procfile |
Heroku process definitions |
Example patterns detected:
# Dockerfile
ENTRYPOINT ["gunicorn", "-c", "config.py", "src.app:app"]
CMD ["celery", "-A", "tasks.celery", "worker"]
ENV FLASK_APP=src/app.py# docker-compose.yml
services:
api:
command: ["python", "-m", "flask", "run"]
worker:
entrypoint: src/run_worker.sh# run_scheduler.sh
python -m celery -A src.tasks.celery beat
python src/run_worker.pyShell scripts referenced by ENTRYPOINT or entrypoint are automatically followed to extract the actual Python commands.
MIT