Back to skills
extension
Category: Development & EngineeringNo API key required

python-dead-code

Detect and remove unused Python code using vulture and ruff. Covers unused imports, variables, functions, classes, and unreachable code. Framework-aware false positive handling for Django, FastAPI, pytest, click, and more. Use when cleaning up Python codebases, enforcing import hygiene, or integrating dead code checks into CI.

personAuthor: jakexiaohubgithub

Python Dead Code Detection

Detect and remove unused code in Python projects using vulture and ruff. Framework-aware analysis that distinguishes real dead code from convention-driven usage patterns.

Core Expertise

What it detects

  • Unused imports (ruff F401)
  • Unused variables (ruff F841)
  • Redefined-unused names (ruff F811)
  • Unused functions and classes (vulture)
  • Unreachable code after return/raise/break (vulture)

Key capabilities

  • Two-tool approach: ruff for fast lint-level checks, vulture for deeper analysis
  • Framework-aware false positive filtering
  • Confidence scoring (vulture --min-confidence)
  • Whitelist support for intentional exceptions
  • CI/CD integration (GitHub Actions, pre-commit)

Installation

# Using uv (recommended)
uv tool install vulture
uv tool install ruff

# Or via pip
pip install vulture ruff

# Verify installation
uv run vulture --version 2>/dev/null || vulture --version
uv run ruff --version 2>/dev/null || ruff --version

Basic Usage

Ruff (Fast Lint-Level Checks)

# Unused imports (F401)
ruff check [target] --select F401

# Unused variables (F841)
ruff check [target] --select F841

# Redefined unused names (F811)
ruff check [target] --select F811

# All unused-code rules together
ruff check [target] --select F401,F811,F841

# Auto-fix safe removals (imports only)
ruff check [target] --select F401 --fix

# Preview what would be fixed
ruff check [target] --select F401 --fix --diff

Vulture (Deep Unused Code Detection)

# Full scan with default confidence (60%)
vulture [target]

# Higher confidence -- fewer false positives (recommended)
vulture [target] --min-confidence 80

# Very high confidence -- only obvious dead code
vulture [target] --min-confidence 90

# Scan with whitelist
vulture [target] whitelist.py --min-confidence 80

# Exclude specific paths
vulture [target] --exclude "venv,__pycache__,.git,migrations"

# Sort by confidence (highest first)
vulture [target] --min-confidence 80 --sort-by-size

Categorizing Findings

Group results into these categories for structured reporting:

| Category | Source | Rule/Detection | |----------|--------|----------------| | Unused imports | ruff | F401 | | Unused variables | ruff | F841 | | Redefined-unused | ruff | F811 | | Unused functions | vulture | unused function | | Unused classes | vulture | unused class | | Unused properties | vulture | unused property | | Unreachable code | vulture | unreachable code |

Interpreting Vulture Output

src/utils.py:42: unused function 'calculate_tax' (60% confidence)
src/models.py:15: unused class 'LegacyUser' (90% confidence)
src/views.py:8: unused import 'render' (90% confidence)
  • 90-100% confidence: Almost certainly dead code
  • 80-89% confidence: Very likely dead code, worth investigating
  • 60-79% confidence: Possible dead code, check for dynamic usage

False Positive Handling

Framework Conventions

These patterns are NOT dead code even if vulture flags them:

Django

  • Views referenced in urls.py via string paths
  • Model fields (accessed via ORM, not direct attribute access)
  • Signal handlers connected via @receiver
  • Management commands (handle method)
  • Admin classes registered with @admin.register
  • Middleware classes referenced in MIDDLEWARE setting
  • Template tags and filters
  • Form/serializer fields

FastAPI / Flask

  • Route handlers decorated with @app.get, @app.post, etc.
  • Dependency injection functions used in Depends()
  • Event handlers (@app.on_event)
  • Exception handlers

pytest

  • Fixtures decorated with @pytest.fixture (used by name injection)
  • Conftest fixtures (auto-discovered)
  • Parametrize arguments
  • Plugin hooks (pytest_* functions)

click / typer

  • Commands decorated with @cli.command()
  • Callback functions
  • Parameter callbacks

General Python

  • __all__ exports
  • __init__, __str__, __repr__, and other dunder methods
  • getattr / importlib dynamic access
  • Abstract method implementations
  • Celery tasks decorated with @app.task or @shared_task
  • Pydantic model fields and validators

Creating Whitelist Files

# whitelist.py -- tell vulture these are intentionally used

# Django views (referenced via urls.py string paths)
index_view  # unused function
detail_view  # unused function

# pytest fixtures
db_session  # unused function
mock_client  # unused function

# Celery tasks
send_notification_email  # unused function

# Signal handlers
on_user_created  # unused function

Generate a whitelist automatically:

# Vulture can generate a whitelist from its findings
vulture [target] --make-whitelist > whitelist.py

Then review and keep only the intentional entries.

Configuration

pyproject.toml (ruff)

[tool.ruff.lint]
# Enable unused-code rules
select = ["F401", "F811", "F841"]

[tool.ruff.lint.per-file-ignores]
# Allow unused imports in __init__.py (re-exports)
"__init__.py" = ["F401"]
# Allow unused imports in conftest.py (fixtures)
"conftest.py" = ["F401"]
# Allow unused imports in type stubs
"*.pyi" = ["F401"]

pyproject.toml (vulture)

[tool.vulture]
min_confidence = 80
exclude = [
    "venv/",
    ".venv/",
    "__pycache__/",
    "migrations/",
    "node_modules/",
]
paths = ["src/", "app/"]

.vulture_whitelist.py

# Symbols that vulture flags but are used dynamically
from myapp.models import *  # noqa: F403 -- re-export

Cleanup Workflow

Apply removals in safest-first order:

1. Unused imports (safest)

# Auto-fix with ruff
ruff check [target] --select F401 --fix

# Or remove manually via editor

Verify: python -c "import mymodule" still works.

2. Unused variables

# Review each finding
ruff check [target] --select F841

For unpacking, replace with _:

# Before
x, y, z = get_coords()  # z unused

# After
x, y, _ = get_coords()

3. Unused functions and classes (riskiest)

For each vulture finding with >= 80% confidence:

  1. Search for dynamic usage: grep -r "function_name" across the codebase
  2. Check __all__ exports: is it in a public API?
  3. Check decorators: @receiver, @app.task, @pytest.fixture, etc.
  4. Check string references: URLs, configs, serializers referencing by name
  5. If truly unused: delete the definition

4. Unreachable code

Remove code blocks after unconditional return, raise, break, or continue.

CI/CD Integration

GitHub Actions

name: Dead Code Check
on:
  push:
    branches: [main]
  pull_request:

jobs:
  dead-code:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: astral-sh/setup-uv@v4

      - name: Check unused imports
        run: uvx ruff check . --select F401,F841 --output-format github

      - name: Check dead code (vulture)
        run: uvx vulture src/ --min-confidence 90

Pre-commit Hook

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.9.0
    hooks:
      - id: ruff
        args: [--select, "F401,F841", --fix]

  - repo: https://github.com/jendrikseipp/vulture
    rev: v2.14
    hooks:
      - id: vulture
        args: [--min-confidence, "90"]

Makefile Target

.PHONY: dead-code
dead-code:
	ruff check . --select F401,F811,F841
	vulture src/ --min-confidence 80

Common Patterns

Check Only Imports (Fastest)

ruff check . --select F401

Use in CI for strict import hygiene.

Full Dead Code Audit

# Step 1: fast lint checks
ruff check . --select F401,F811,F841

# Step 2: deep analysis
vulture . --min-confidence 80 --exclude "venv,migrations,__pycache__"

# Step 3: cross-reference and filter false positives

Monorepo / Multi-package

# Scan specific packages
vulture packages/core/ --min-confidence 80
vulture packages/api/ --min-confidence 80

# Or scan all with shared whitelist
vulture packages/ whitelist.py --min-confidence 80

Library Development

# Be conservative -- public API symbols may appear unused
vulture src/mylib/ --min-confidence 90

# Exclude __init__.py re-exports
ruff check src/mylib/ --select F401 --per-file-ignores "__init__.py:F401"

Troubleshooting

Too Many False Positives

  • Increase --min-confidence to 90
  • Create a whitelist file for framework-specific patterns
  • Use --exclude for generated code, migrations, vendored code

Vulture Missing Files

# Ensure target path is correct
vulture src/ tests/ --min-confidence 80

# Debug: list what vulture scans
vulture src/ --min-confidence 80 2>&1 | head -20

Ruff Not Detecting Unused Imports

# Check if F401 is enabled
ruff check --show-settings | grep F401

# Force enable
ruff check . --select F401 --config "lint.select = ['F401']"

Conflict Between Ruff and Vulture

Ruff and vulture may flag the same import. Use ruff for imports (auto-fixable) and vulture for functions/classes (needs manual review). Deduplicate findings before presenting.

References

  • Vulture docs: https://github.com/jendrikseipp/vulture
  • Ruff F rules: https://docs.astral.sh/ruff/rules/#pyflakes-f
  • Ruff configuration: https://docs.astral.sh/ruff/configuration/