On this page
- Types of dead code
- Unreachable files
- Unused exports
- Zombie packages
- How modern detectors work
- Confidence scoring
- Cross-language graph traversal
- Python: Vulture and deadcode
- Vulture
- deadcode
- JavaScript and TypeScript: Knip, and where ts-prune stands
- Knip
- ts-prune
- Go: deadcode and staticcheck
- deadcode
- staticcheck U1000
- Java: IDE inspections, PMD and UCDetector
- C#: Roslyn analyzers
- Unused dependencies: the dead code in your manifest
- Mixed-language repos: repowise getdeadcode
- Dead-code detection in IDEs
- Side-by-side comparison
- Cleanup playbook
- 1. Start with one signal type
- 2. Sort by confidence
- 3. Check for framework entry points
- 4. Review ownership and history
- 5. Remove in small patches
- 6. Add guardrails
- Final recommendation
- How I checked
- FAQ
Dead code analysis means finding code and dependencies you can delete without changing behavior. The right tool depends on the language: Vulture or deadcode for Python, Knip for JavaScript and TypeScript, Go's deadcode command, IntelliJ inspections or PMD for Java, Roslyn analyzers for C#. For repos that mix languages, use a graph-based tool, then review every finding before deleting.
| Language | Start with | Also worth knowing | Finds unused dependencies? |
|---|---|---|---|
| Python | Vulture | deadcode (has --fix) | No, use deptry |
| JavaScript / TypeScript | Knip | ts-prune (unused exports only) | Yes, Knip does |
| Go | deadcode | staticcheck U1000 | go mod tidy covers most of it |
| Java | IntelliJ "Unused declaration" inspection | PMD Unused* rules, UCDetector | No |
| C# | Roslyn rules IDE0051 / IDE0052 | IDE and ReSharper inspections | No |
| Rust | the compiler's dead_code lint | cargo-udeps, cargo-machete | |
| Mixed-language repo | repowise get_dead_code | the per-language tools above | Unused monorepo packages |
Scroll the table sideways to see every column.
Unreachable files, unused exports and zombie packages all need different heuristics, and a tool that flags one well may miss the others. Below I go language by language, then cover unused dependencies, then the cleanup workflow that keeps false positives under control.
We build repowise, so weigh our entry accordingly. The measurements we publish about it, with their methods, are on the benchmarks page.
Types of dead code
Dead code is code you can delete without changing behavior. That sounds simple until you look at a real repo. Static analyzers, compilers, and IDE inspections all disagree on what counts as dead, because they make different assumptions about entry points, reflection, config files, and generated code.
Unreachable files
An unreachable file is a file that no runtime path can import or load. In a JS app, this might be a utility module left behind after a refactor. In Python, it might be a helper that nothing imports. In a monorepo, the same file can be reachable from one package and dead from another.
Tools that read one file at a time miss this, because only a project graph shows which files can be reached from an entry point.
Unused exports
Unused exports are the easiest place to start. These are functions, classes, types, or constants that other files never import.
Knip focuses on exported values and types across files; it does not report unused locals inside a file. Its docs also define unused files as project files that are not resolved from entry files, which is the right model for codebase-level cleanup. (knip.dev)
Zombie packages
Zombie packages are dependencies that still sit in package.json, requirements.txt, or similar manifests even though the code no longer imports them. An unused dependency still costs build time, adds security surface and makes upgrades noisier. The section on unused dependencies below covers the tools that find them.
How modern detectors work
Older detectors scanned text with a few syntax heuristics, while the better tools build a graph of the project.
Confidence scoring
A good dead code tool should tell you how sure it is. Vulture attaches a confidence value to every finding, from 60% for unused functions and attributes up to 100% for unreachable code, and --min-confidence 100 limits the report to the findings it is certain about. (pypi.org) That flag gives you a short list you can act on before you review the less certain findings.
repowise's get_dead_code is built around the same idea: findings are tiered high, medium or low confidence, and they sit on top of graph, git and context layers, so a finding can be paired with ownership, history, and dependency impact. (repowise.dev)
Cross-language graph traversal
The main reason dead code gets tricky is that modern repos are not single-language. A TypeScript frontend calls a Python API, which shells out to Go, which loads config from YAML. A file can look unused until you include generated code, dynamic imports, and framework conventions.
Every per-language tool below stops at its language boundary. That is fine for a single-language repo and a real gap in a polyglot one.
Python: Vulture and deadcode
Vulture
Vulture is the classic Python dead-code detector. It uses Python's AST to find unused classes, functions, methods, variables, imports, attributes and unreachable code. Version 2.16 shipped in March 2026 and needs Python 3.9 or newer. (pypi.org)
Reasons teams still use it:
- It is lightweight and easy to run in CI.
- Confidence values let you start with the safe 100% findings.
--make-whitelistturns today's false positives into a file you check in, so the next run only shows new findings.
Where it struggles:
- dynamic imports and
getattr - decorators and framework magic (Django views, FastAPI routes, Celery tasks, pytest fixtures)
- anything reached only from config or templates
Treat Vulture as a triage tool: run it, read the list and confirm each candidate before you delete anything.
deadcode
deadcode is a newer Python tool aimed at whole-codebase checks that Ruff and flake8 do not do (they only look at unused locals). It reports unused variables, functions, classes, methods, attributes, imports, empty files and unreachable blocks under DCxxx codes, and it can delete what it finds with deadcode . --fix, or show the diff first with --fix --dry. (github.com)
I would use --dry first every time. Auto-fix is only as good as the tool's view of your entry points, and Python hides a lot of entry points.
JavaScript and TypeScript: Knip, and where ts-prune stands
Knip
Knip is the strongest open source choice for JS/TS dead-code cleanup. It finds unused files, unused exports and unused dependencies in one pass, and it ships 150+ plugins for frameworks and tools like Next.js, Vite, Vitest, Jest, Nx, Storybook and GitHub Actions, so it knows where their entry points live. (knip.dev)
Why I like it:
- It knows the difference between unused files and unused exports.
- It can remove unused files and exports with auto-fix when you are ready. (knip.dev)
- It handles monorepos and workspaces. (knip.dev)
The tradeoff is the one that comes with any smart static analyzer: you must configure entry files correctly. Miss an entry point and Knip reports used code as unused. Make entry files too broad and you hide dead code. (knip.dev)
ts-prune
ts-prune is simpler. It reads your tsconfig and prints unused exports, nothing else. (npmjs.com) Its README now says the project "has been resurrected after a long hiatus" and is kept working because many projects still depend on it. (github.com) So it is maintained, but in a keep-it-running way.
I would not start a new codebase on it. Knip covers exports plus files plus dependencies. ts-prune still makes sense when you only want an unused-export list and already have other tools for the rest.
Go: deadcode and staticcheck
deadcode
The Go team published deadcode in December 2023 (go install golang.org/x/tools/cmd/deadcode@latest). It starts from each main package and uses Rapid Type Analysis, a whole-program analysis, to work out which functions can ever be reached. Everything else is reported as unreachable.
Two limits matter. It needs a main entry point, so for a library you run it with -test and let your tests be the entry points. And it cannot see functions called only from assembly or through go:linkname. (go.dev)
staticcheck U1000
Staticcheck's U1000 check reports unused functions, types, fields and constants. It was rewritten in Staticcheck 2023.1 with fewer false positives, including fixes for generics. It treats every exported package-level name as used, which keeps it quiet on libraries, and it also runs inside golangci-lint as the unused linter. (staticcheck.io)
Use U1000 in CI for the per-package view and deadcode occasionally for the whole-program view, because the two checks answer different questions.
Java: IDE inspections, PMD and UCDetector
Java has no single standard CLI for whole-program dead code, so most teams combine an IDE inspection, a CI rule set and sometimes an Eclipse plugin.
- IntelliJ's "Unused declaration" inspection works best when you run it project-wide (Code, Analyze Code, Run Inspection by Name) as well as in the editor. You can tell it which annotations mark entry points, which matters for Spring, JAX-RS and test frameworks. (JetBrains)
- PMD rules like
UnusedPrivateMethod,UnusedPrivateField,UnusedLocalVariable,UnusedFormalParameterandUnusedAssignmentrun in CI on every build. They only cover private and local code, which is exactly why they are safe to enforce. (pmd.github.io) - UCDetector is an open source Eclipse plugin that finds unused public code and suggests narrowing visibility. It is old (2.0.0 is the latest release on SourceForge), but for an Eclipse shop it still answers the "is this public method used anywhere" question.
Reflection, dependency injection and serialization are the usual false positives in Java. Tell the tool about your framework annotations before trusting the list.
C#: Roslyn analyzers
For .NET, start with the analyzers that ship with the SDK. IDE0051 flags unused private members and IDE0052 flags private members that are written but never read. (Microsoft Learn) Set them to warning in .editorconfig and they run in every build. Public and internal code that nothing uses needs a solution-wide pass, which Rider or ReSharper can do.
Unused dependencies: the dead code in your manifest
A dependency nobody imports still gets installed, scanned and upgraded. These tools compare what your manifest declares with what your code imports.
| Ecosystem | Tool | What it reports | Status |
|---|---|---|---|
| JS / TS | Knip | Unused and unlisted dependencies, plus unused files and exports | Actively maintained |
| JS / TS | depcheck | Unused dependencies | No longer maintained; its README recommends Knip |
| Python | deptry | Missing (DEP001), unused (DEP002) and misplaced dev (DEP004) dependencies | Works with pip, Poetry, PDM, uv and PEP 621 |
| Rust | cargo-udeps | Unused crates, from compiler output | Builds on stable, needs nightly to run |
| Rust | cargo-machete | Unused crates, by scanning source | Fast and stable Rust, but imprecise by its own description |
| Go | go mod tidy | Removes modules nothing imports | Built in |
Scroll the table sideways to see every column.
If you still run depcheck in CI, switching to Knip is a cheap win: its maintainers say it "is no longer actively maintained" and point to Knip. (github.com)
For Rust, a common setup is cargo-machete in CI because it is fast, and cargo-udeps occasionally for the precise answer.
Mixed-language repos: repowise get_dead_code
Best for polyglot repos, large monorepos and teams that want dead-code findings with context.
Every tool above stops at its language. If your repo has a TypeScript frontend, a Python service and shared packages, you either run four tools and merge the results by hand, or you use something that reads the whole repo as one graph.
That second option is what we built get_dead_code in repowise for. It finds unreachable files, unused exports and unused monorepo packages from the dependency graph, tiers each finding by confidence, marks the ones that look safe to delete, and rolls them up by directory or owner. Because it is an MCP tool, a coding agent can ask for dead-code candidates in the same session as ownership, risk and dependency paths, which matters when a cleanup touches shared code. (repowise.dev) repowise is open source under AGPL-3.0 and self-hostable.
Inside their own languages, Knip and Vulture know framework conventions in more depth. I would use repowise for the cross-language view and the context, and keep the language tools in CI.
If you want a concrete output shape, the FastAPI dependency graph and FastAPI docs show what it produces on a real codebase, and live examples cover more repos.
The architecture map for repowise-dev/repowise. The Dead filter below the graph picks out the files the dead-code pass flagged, in place among the files that import them.
Dead Code Detection Comparison Table
Dead-code detection in IDEs
Best for local edits and small cleanups.
IDE inspections are good feedback while you edit. They are not enough for repo-wide cleanup, especially when your app uses reflection, entry points are spread across config files, or you need reachability across packages. Use them for the file in front of you and a standalone tool for the audit.
Side-by-side comparison
| Tool | Language | Unused exports / members | Unused files | Unused deps | Auto-fix | Notes |
|---|---|---|---|---|---|---|
| Vulture | Python | Yes | No | No | No | Confidence values, whitelist file |
| deadcode | Python | Yes | Empty files | No | Yes (--fix) | Whole-codebase checks Ruff lacks |
| deptry | Python | No | No | Yes | No | Missing, unused, misplaced |
| Knip | JS / TS | Yes | Yes | Yes | Yes | 150+ framework plugins |
| ts-prune | TS | Exports only | No | No | No | Maintained to keep working |
deadcode (Go) | Go | Unreachable functions | No | No | No | Whole-program, needs main or -test |
| staticcheck U1000 | Go | Yes | No | No | No | Exported names count as used |
| PMD Unused* rules | Java | Private and local only | No | No | No | Safe to enforce in CI |
| IntelliJ inspection | Java, Kotlin | Yes | Partly | No | Quick-fix | Configure entry-point annotations |
| Roslyn IDE0051/52 | C# | Private members | No | No | Code fix | Ships with the SDK |
repowise get_dead_code | Many | Yes | Yes | Monorepo packages | No | Confidence tiers, owner rollups, cross-language |
Scroll the table sideways to see every column.
Cross-Language Dead Code Graph
Cleanup playbook
Dead-code cleanup goes wrong when teams jump straight to deletion, so use a staged process. The find dead code guide walks through it on a real repo.
1. Start with one signal type
Pick either unused exports or unused files. Starting with one signal type keeps the review small enough to finish.
2. Sort by confidence
Delete obvious leaf code first:
- private helpers no one imports
- stale feature flags
- abandoned modules with no dependents
- dead packages with no runtime or build references
3. Check for framework entry points
Watch for:
- route files
- reflection-based lookups
- CLI subcommands
- plugin registries
- test-only imports
4. Review ownership and history
Before you remove a shared module, check who owns it and how often it changes. A file nobody has touched in two years and a file that changed last week are different conversations, even if both look unused. repowise's git-intelligence layer shows hotspots and ownership next to the finding.
5. Remove in small patches
Small patches are easier to revert, and they make it obvious which change broke CI. If you use a tool with auto-fix, run it on one package or directory at a time.
6. Add guardrails
After cleanup:
- keep the detector in CI
- add a baseline or allowlist for known exceptions
- review new dead-code hits in PRs
If you want the repo context around the cleanup, the FastAPI code health page shows how churn and complexity interact, and the ownership map for FastAPI shows the history that tells you who to ask before a refactor.
Final recommendation
If you need the shortest path:
- Python: Vulture, plus deptry for packages
- JS/TS: Knip
- Go: staticcheck U1000 in CI,
deadcodefor a whole-program pass - Java: IntelliJ inspection project-wide, PMD in CI
- C#: IDE0051 and IDE0052 as warnings
- Mixed-language or context-heavy repos: repowise
get_dead_codeon top of the language tools
Whatever you pick, do not stop at a list of filenames. Pair findings with dependency paths, ownership and history before you delete.
To see it on your own code, paste a public repo URL on the homepage or run pip install repowise && repowise init locally. If the cleanup is a team effort, repowise for teams covers shared indexes and owner rollups.
How I checked
Tool status, versions and features come from each project's own README, docs or release notes, read on October 6, 2026. I did not run a head-to-head accuracy benchmark across these tools, so nothing here claims one finds more true dead code than another. False-positive rates depend heavily on how well you configure entry points.
For the full picture, see the cluster hub: How to prioritize technical debt (impact-per-effort).
FAQ
What is dead code analysis?
Dead code analysis finds code that can never run or is never used: unreachable functions, unused exports and private members, files nothing imports, and dependencies nothing references. Static tools do it by building a graph from entry points. Runtime tools, like coverage in production, find code that is reachable but never actually called.
What is the best dead code detection tool for Python?
Vulture is the standard starting point, with confidence values and a whitelist file for false positives. deadcode is a newer option that can also delete what it finds with `--fix`. For unused packages, add deptry. ([pypi.org](https://pypi.org/project/vulture/))
What is the best dead code detection tool for JavaScript and TypeScript?
Knip. It finds unused files, exports and dependencies in one pass, understands monorepos, and has plugins for most frameworks. ([knip.dev](https://knip.dev/))
Is ts-prune deprecated?
Not officially. Its README says it was revived after a long hiatus and is kept working because many projects depend on it. For new projects Knip covers more: files and dependencies as well as exports. ([github.com](https://github.com/nadeesha/ts-prune))
How do I find unused dependencies?
Use Knip for JavaScript and TypeScript, deptry for Python, cargo-machete or cargo-udeps for Rust, and `go mod tidy` for Go. Avoid depcheck in new setups, since its maintainers say it is no longer maintained.
How do I find dead code in Go?
Run `deadcode` from `golang.org/x/tools/cmd/deadcode` for whole-program reachability from your `main` packages, and staticcheck's U1000 (or golangci-lint's `unused` linter) in CI for unused code per package. ([go.dev](https://go.dev/blog/deadcode))