Skip to content

Latest commit

Β 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation


πŸ’€

R E A P E R
Find dead Python code before it finds you.

Built with Rust 9 Rules ~3ms average Cross-file analysis MIT License


Reaper is a blazing-fast, zero-config dead code finder for Python β€” built in Rust. It scans your entire project in milliseconds and catches what other tools miss: unused imports, dead functions, unreachable code, phantom classes, and more β€” across file boundaries.

One command. Nine rules. Zero configuration required.

reaper

What is Reaper?

Reaper reads your entire project at once and cross-references every definition against every usage β€” so it catches dead code that single-file tools structurally cannot see.

  • 9 rules β€” imports, variables, functions, classes, unreachable code, dead branches, redefined imports, arguments, loop variables
  • Cross-file analysis β€” a function defined in utils.py but called nowhere is flagged project-wide
  • Parallel β€” every file is analysed concurrently via Rayon; per-file checkers also run in parallel
  • Zero config β€” works out of the box; respects .gitignore automatically
  • # noqa support β€” suppress any rule inline or by code
  • JSON output β€” machine-readable results for CI pipelines

⚑ Quickstart

Install pre-built binary (recommended)

Pre-built binaries are published on every version bump to GitHub Releases.

Linux / macOS (single command):

curl -fsSL https://raw.githubusercontent.com/taradepan/reaper/main/install.sh | sh

Windows (PowerShell):

irm https://raw.githubusercontent.com/taradepan/reaper/main/install.ps1 | iex

Install from source

# Clone and build
git clone https://github.com/taradepan/reaper.git
cd reaper
cargo build --release

# The binary is at ./target/release/reaper
# Optionally, put it on your PATH:
cp ./target/release/reaper /usr/local/bin/

Install via Cargo

cargo install --path .

Run it

# Scan the current directory (these are identical)
reaper
reaper .

# Scan specific files or directories
reaper src/ lib/utils.py scripts/

# Only check specific rules
reaper --select RP001,RP003 .

# Exclude test directories
reaper --exclude tests,migrations,fixtures .

# JSON output for CI pipelines
reaper --json .

# Don't fail CI β€” just report
reaper --json --no-exit-code .

🎯 What It Looks Like

Give Reaper a Python file like this:

import os                          # ← never used
import sys                         # ← never used
import json
from collections import OrderedDict, defaultdict  # OrderedDict never used

def fetch_users(db, timeout):      # ← 'timeout' arg never used
    unused_config = {"retries": 3} # ← assigned, never read
    return db.execute("SELECT *")

def deprecated_endpoint():         # ← nobody calls this function
    return None

class LegacyParser:                # ← nobody instantiates this class
    def parse(self, raw):
        return raw.split(",")

def process_batch(items):
    for item in items:
        item.save()
    return True
    remaining = items[:]           # ← unreachable after return
    return remaining

if False:                          # ← dead branch, never executes
    debug_mode = True

for idx in range(10):              # ← 'idx' is never used
    print("processing...")

Reaper outputs:

demo.py:1:8:   RP001 `os` imported but unused
demo.py:2:8:   RP001 `sys` imported but unused
demo.py:4:25:  RP001 `OrderedDict` imported but unused
demo.py:6:21:  RP008 Argument `timeout` is not used
demo.py:7:5:   RP002 Local variable `unused_config` is assigned but never used
demo.py:11:1:  RP003 Function `deprecated_endpoint` is defined but never used
demo.py:14:1:  RP004 Class `LegacyParser` is defined but never used
demo.py:22:5:  RP005 Code is unreachable
demo.py:25:1:  RP006 `if False:` branch is never executed
demo.py:29:5:  RP009 Loop variable `idx` is not used
Found 10 issue(s)

All 9 rules fired. Time: <5ms. ⚑


πŸ“ Rules

Reaper ships with 9 purpose-built dead-code rules:

RP001 β€” Unused Import

import os          # RP001 β€” `os` imported but unused
import json        # βœ… OK β€” used below

data = json.loads('{}')

Respects __all__, TYPE_CHECKING guards, and __future__ imports.


RP002 β€” Unused Variable

def calculate():
    temp = 42       # RP002 β€” assigned but never read
    result = 100
    return result

Smart about augmented assignments (total += 1), walrus operators (:=), and comprehension variables.


RP003 β€” Unused Function (Cross-File) 🌐

# utils.py
def helper():        # βœ… OK β€” called from main.py
    return 42

def orphan():        # RP003 β€” defined but never called from anywhere
    return 0

RP003 scans your entire project β€” if no file imports or calls orphan(), it's dead.


RP004 β€” Unused Class (Cross-File) 🌐

class UserSerializer:     # βœ… OK β€” instantiated in views.py
    pass

class LegacyParser:       # RP004 β€” never instantiated or referenced anywhere
    pass

Same cross-file analysis as RP003 β€” project-wide dead class detection.


RP005 β€” Unreachable Code

def process():
    return True
    cleanup()        # RP005 β€” Code is unreachable
    log("done")      # RP005 β€” Code is unreachable

Detects code after return, raise, break, and continue β€” not just "unused variable" but the semantically richer "this code can never execute".


RP006 β€” Dead Branch

if False:                     # RP006 β€” branch never executes
    enable_debug()

if None:                      # RP006 β€” branch never executes
    setup_logging()

from typing import TYPE_CHECKING
if TYPE_CHECKING:             # RP006 β€” correctly identified as dead at runtime
    import heavy_module       # (but NOT flagged as RP001 β€” Reaper knows this is intentional)

RP007 β€” Import Redefined Before Use

import os                    # RP007 β€” overwritten before it's ever read
os = "not the module anymore"

import sys                   # βœ… OK β€” used before reassignment
print(sys.version)
sys = "overwritten later"

RP008 β€” Unused Function Argument

def send_email(to, subject, priority):   # RP008 β€” `priority` is never used
    return mailer.send(to=to, subject=subject)

def callback(_event, data):              # βœ… OK β€” underscore prefix = intentionally unused
    return process(data)

class Base(ABC):
    @abstractmethod
    def handle(self, request):           # βœ… OK β€” abstract methods are skipped
        ...

Respects _-prefixed arguments, *args, **kwargs, self, cls, and abstract methods.


RP009 β€” Unused Loop Variable

for i in range(10):          # RP009 β€” `i` is never used
    print("tick")

for _ in range(10):          # βœ… OK β€” underscore convention
    print("tick")

for key, value in data.items():  # βœ… OK β€” both used
    result[key] = transform(value)

πŸ”— Cross-File Analysis

This is Reaper's killer feature. Most linters are single-file: they can tell you os is unused on line 1, but they cannot tell you that generate_report() defined in utils.py is never called from anywhere in your codebase. Reaper can.

How it works:

Project/
β”œβ”€β”€ utils.py      β†’  defines fetch_users(), sync_inventory(), generate_report()
β”œβ”€β”€ models.py     β†’  defines User, CacheManager
└── main.py       β†’  imports fetch_users, User
$ reaper --select RP003,RP004 Project/

utils.py:5:1:  RP003 Function `sync_inventory` is defined but never used
utils.py:9:1:  RP003 Function `generate_report` is defined but never used
models.py:8:1: RP004 Class `CacheManager` is defined but never used
Found 3 issue(s)

Reaper builds a global usage set across all files in one parallel pass. A function defined in utils.py but called from main.py is correctly recognized as alive. A function that nobody anywhere calls β€” that's dead code.

Note: Cross-file analysis uses name-based matching, not full import-graph resolution. It errs on the side of false negatives (missing some dead code) rather than false positives (never flags live code as dead).


⚑ Performance

Measured with hyperfine --warmup 5 --runs 30 on Apple Silicon.

Corpus Files Time
Small 20 2.6 ms Β± 0.3
Medium 8 3.1 ms Β± 0.4
Large 4 3.2 ms Β± 0.3
Edge cases 15 2.1 ms Β± 0.3
All 47 4.6 ms Β± 0.3

Why is it fast?

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Reaper Core                     β”‚
β”‚                                                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚  β”‚  Zero-copy   │───▢│  Single-pass  β”‚            β”‚
β”‚  β”‚   Lexer      β”‚    β”‚   AST Build   β”‚            β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β”‚                            β”‚                      β”‚
β”‚            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚            β–Ό               β–Ό               β–Ό      β”‚
β”‚     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚     β”‚ Per-file  β”‚   β”‚ Per-file  β”‚   β”‚ Per-file  β”‚  β”‚
β”‚     β”‚ Checks   β”‚   β”‚ Checks   β”‚   β”‚ Checks   β”‚   β”‚
β”‚     β”‚ (Rayon)  β”‚   β”‚ (Rayon)  β”‚   β”‚ (Rayon)  β”‚   β”‚
β”‚     β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜   β”‚
β”‚          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β”‚
β”‚                          β–Ό                        β”‚
β”‚               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”‚
β”‚               β”‚   Cross-file     β”‚                β”‚
β”‚               β”‚  RP003 / RP004   β”‚                β”‚
β”‚               β”‚  (Global merge)  β”‚                β”‚
β”‚               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  1. Custom zero-copy lexer β€” Tokens borrow directly from the source string. No heap allocations during lexing.
  2. Single-pass AST β€” The parser builds a typed AST in one linear pass. No backtracking.
  3. Parallel per-file analysis β€” Every file is analyzed concurrently via Rayon. Within each file, all 6 per-file checkers run in parallel too.
  4. Two-pass architecture β€” Pass 1 (parallel): per-file checks + collect defs/usages. Pass 2 (parallel merge): cross-file RP003/RP004 against the global usage set.

πŸ”‡ Suppressing Diagnostics

Inline with # noqa

import os           # noqa            β€” suppress ALL rules on this line
import sys          # noqa: RP001     β€” suppress only RP001
import re           # noqa: RP001, RP007  β€” suppress RP001 and RP007

With --select (only run specific rules)

# Only check for unused imports and dead functions
reaper --select RP001,RP003 .

With --exclude (skip directories)

# Skip tests, migrations, and generated code
reaper --exclude tests,migrations,generated .

Auto-excluded directories

These are always skipped β€” you never need to list them manually:

.git Β· .hg Β· .svn Β· .venv Β· .env Β· venv Β· env Β· virtualenv Β· __pycache__ Β· .mypy_cache Β· .ruff_cache Β· .pytest_cache Β· .hypothesis Β· .tox Β· .nox Β· build Β· dist Β· .eggs Β· node_modules


πŸ€– CI Integration

GitHub Actions (pre-built binary β€” fast ⚑)

No Rust toolchain needed. Downloads the pre-built binary from GitHub Releases in ~2 seconds.

name: Dead Code Check

on: [push, pull_request]

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

      - name: Install Reaper
        run: curl -fsSL https://raw.githubusercontent.com/taradepan/reaper/main/install.sh | sh

      - name: Find dead code
        run: reaper --exclude tests .

GitHub Actions (build from source)

If you prefer to always build from the latest commit:

name: Dead Code Check

on: [push, pull_request]

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

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Install Reaper
        run: cargo install --git https://github.com/taradepan/reaper.git

      - name: Find dead code
        run: reaper --exclude tests .

GitHub Actions (JSON + inline annotations)

      - name: Find dead code (JSON)
        run: |
          reaper --json --no-exit-code . > reaper-report.json
          cat reaper-report.json | jq -r \
            '.diagnostics[] | "::warning file=\(.file),line=\(.line),col=\(.col)::\(.code) \(.message)"'

Pre-commit hook

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: reaper
        name: reaper (dead code)
        entry: reaper --exclude tests
        language: system
        types: [python]
        pass_filenames: false

Makefile

.PHONY: lint lint-json
lint:
	reaper --exclude tests,migrations .

lint-json:
	reaper --json --no-exit-code --exclude tests . > dead-code-report.json

πŸ–₯️ CLI Reference

reaper [OPTIONS] [PATHS]...
Flag Description Example
PATHS Files or directories to scan (default: current dir) reaper src/ lib/
--select CODES Only run specific rules (comma-separated) --select RP001,RP003
--exclude NAMES Skip paths containing these names --exclude tests,vendor
--json Output results as structured JSON --json
--no-exit-code Always exit 0, even with findings --no-exit-code
-h, --help Show help -h
-V, --version Print version -V

Exit codes

Code Meaning
0 No issues found (or --no-exit-code was passed)
1 Dead code found
2 Runtime error (bad path, permission denied, etc.)

JSON output format

{
  "count": 3,
  "diagnostics": [
    {
      "file": "src/utils.py",
      "line": 1,
      "col": 8,
      "code": "RP001",
      "message": "`os` imported but unused"
    },
    {
      "file": "src/utils.py",
      "line": 14,
      "col": 1,
      "code": "RP003",
      "message": "Function `orphan` is defined but never used"
    },
    {
      "file": "src/models.py",
      "line": 22,
      "col": 1,
      "code": "RP004",
      "message": "Class `LegacyParser` is defined but never used"
    }
  ]
}

πŸ—οΈ Architecture

reaper/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ main.rs            # CLI (clap), orchestration, output formatting
β”‚   β”œβ”€β”€ lib.rs             # Public library interface
β”‚   β”œβ”€β”€ analyze.rs         # Two-pass analysis engine (per-file βˆ₯ cross-file)
β”‚   β”œβ”€β”€ discovery.rs       # .py file walker (ignore crate, .gitignore-aware)
β”‚   β”œβ”€β”€ fast_parser/
β”‚   β”‚   β”œβ”€β”€ lexer.rs       # Zero-copy Python tokenizer
β”‚   β”‚   └── parser.rs      # Single-pass AST builder
β”‚   β”œβ”€β”€ ast.rs             # Typed AST node definitions
β”‚   β”œβ”€β”€ names.rs           # Name/usage collection walkers
β”‚   β”œβ”€β”€ location.rs        # Byte offset β†’ (line, col) conversion
β”‚   β”œβ”€β”€ types.rs           # Diagnostic, RuleCode types
β”‚   └── checks/
β”‚       β”œβ”€β”€ unused_imports.rs    # RP001
β”‚       β”œβ”€β”€ unused_variables.rs  # RP002
β”‚       β”œβ”€β”€ unused_defs.rs       # RP003, RP004
β”‚       β”œβ”€β”€ unreachable.rs       # RP005
β”‚       β”œβ”€β”€ dead_branch.rs       # RP006 (also handles RP007)
β”‚       β”œβ”€β”€ unused_args.rs       # RP008
β”‚       └── unused_loop_var.rs   # RP009
β”œβ”€β”€ tests/
β”‚   └── integration.rs     # 53 integration tests
β”œβ”€β”€ benches/
β”‚   └── bench_analyze.rs   # Criterion micro-benchmarks
└── benchmark/
    β”œβ”€β”€ gen_corpus.py      # Generate synthetic benchmark corpus
    β”œβ”€β”€ final_benchmark.sh # Hyperfine speed + accuracy vs Ruff
    └── audit_prod.py      # Audit Rust source for production issues

Test suite

cargo test
# 169 unit tests + 53 integration tests + 1 doc-test = 223 total
# All passing βœ…

cargo clippy -- -D warnings
# Clean βœ…

cargo fmt --check
# Clean βœ…

πŸ—ΊοΈ Roadmap

  • 9 dead-code rules (RP001–RP009)
  • Cross-file analysis (RP003, RP004)
  • # noqa inline suppression
  • JSON output
  • --select / --exclude filtering
  • Parallel analysis (Rayon)
  • .gitignore-aware file discovery
  • pyproject.toml / config file support
  • --fix autofix for safe removals (unused imports)
  • --stdin support for editor/IDE integration
  • Glob patterns for --exclude
  • Import-graph resolution for RP003/RP004
  • Published crates.io package
  • Pre-built binaries (GitHub Releases)
  • VS Code extension

🀝 Contributing

Contributions are welcome! Here's how to get started:

# Clone
git clone https://github.com/YOUR_USERNAME/reaper.git
cd reaper

# Run all tests
cargo test

# Lint
cargo clippy -- -D warnings

# Format
cargo fmt

# Build release binary
cargo build --release

# Run micro-benchmarks
cargo bench

Adding a new rule

  1. Create a new checker in src/checks/.
  2. Add the RuleCode variant in src/types.rs.
  3. Wire it into the analysis pipeline in src/analyze.rs.
  4. Add unit tests in the checker file and integration tests in tests/integration.rs.
  5. Document it in this README under Rules.

πŸ“„ License

MIT β€” do whatever you want with it.



πŸ’€
Dead code has nowhere to hide.

Built with Rust Β· Powered by Rayon Β· Faster than you think

About

A fast, zero-false-positive Python dead-code finder written in Rust

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages