💀
R E A P E R
Find dead Python code before it finds you.
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.
reaperReaper 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.pybut 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
.gitignoreautomatically # noqasupport — suppress any rule inline or by code- JSON output — machine-readable results for CI pipelines
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 | shWindows (PowerShell):
irm https://raw.githubusercontent.com/taradepan/reaper/main/install.ps1 | iex# 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/cargo install --path .# 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 .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. ⚡
Reaper ships with 9 purpose-built dead-code rules:
import os # RP001 — `os` imported but unused
import json # ✅ OK — used below
data = json.loads('{}')Respects __all__, TYPE_CHECKING guards, and __future__ imports.
def calculate():
temp = 42 # RP002 — assigned but never read
result = 100
return resultSmart about augmented assignments (total += 1), walrus operators (:=), and comprehension variables.
# utils.py
def helper(): # ✅ OK — called from main.py
return 42
def orphan(): # RP003 — defined but never called from anywhere
return 0RP003 scans your entire project — if no file imports or calls orphan(), it's dead.
class UserSerializer: # ✅ OK — instantiated in views.py
pass
class LegacyParser: # RP004 — never instantiated or referenced anywhere
passSame cross-file analysis as RP003 — project-wide dead class detection.
def process():
return True
cleanup() # RP005 — Code is unreachable
log("done") # RP005 — Code is unreachableDetects code after return, raise, break, and continue — not just "unused variable" but the semantically richer "this code can never execute".
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)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"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.
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)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).
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 |
┌──────────────────────────────────────────────────┐
│ 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) │ │
│ └──────────────────┘ │
└──────────────────────────────────────────────────┘
- Custom zero-copy lexer — Tokens borrow directly from the source string. No heap allocations during lexing.
- Single-pass AST — The parser builds a typed AST in one linear pass. No backtracking.
- Parallel per-file analysis — Every file is analyzed concurrently via Rayon. Within each file, all 6 per-file checkers run in parallel too.
- 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.
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# Only check for unused imports and dead functions
reaper --select RP001,RP003 .# Skip tests, migrations, and generated code
reaper --exclude tests,migrations,generated .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
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 .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 . - 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-config.yaml
repos:
- repo: local
hooks:
- id: reaper
name: reaper (dead code)
entry: reaper --exclude tests
language: system
types: [python]
pass_filenames: false.PHONY: lint lint-json
lint:
reaper --exclude tests,migrations .
lint-json:
reaper --json --no-exit-code --exclude tests . > dead-code-report.jsonreaper [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 |
| Code | Meaning |
|---|---|
0 |
No issues found (or --no-exit-code was passed) |
1 |
Dead code found |
2 |
Runtime error (bad path, permission denied, etc.) |
{
"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"
}
]
}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
cargo test
# 169 unit tests + 53 integration tests + 1 doc-test = 223 total
# All passing ✅
cargo clippy -- -D warnings
# Clean ✅
cargo fmt --check
# Clean ✅- 9 dead-code rules (RP001–RP009)
- Cross-file analysis (RP003, RP004)
-
# noqainline suppression - JSON output
-
--select/--excludefiltering - Parallel analysis (Rayon)
-
.gitignore-aware file discovery -
pyproject.toml/ config file support -
--fixautofix for safe removals (unused imports) -
--stdinsupport 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
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- Create a new checker in
src/checks/. - Add the
RuleCodevariant insrc/types.rs. - Wire it into the analysis pipeline in
src/analyze.rs. - Add unit tests in the checker file and integration tests in
tests/integration.rs. - Document it in this README under Rules.
MIT — do whatever you want with it.
💀
Dead code has nowhere to hide.
Built with Rust · Powered by Rayon · Faster than you think