Short version:
- Choose Vulture if you want a maintained, MIT-licensed dead-code finder that fails CI with exit code 3, and you can maintain a config for your framework. Configured, it was the most complete tool on our Django app.
- Choose deadcode if you want it to delete findings for you (
--fix) and can accept AGPL-3.0, no release since August 2024, and exit code 0 on findings. Read its preview: on our Flask app it would have deleted live routes. - Choose Skylos if you want framework handling built in (it flagged the least live code with defaults), transitive dead code and a removal preview, or security checks in the same run. Know its gaps: Flask
before_app_requesthooks, unused Django class-based views and@login_requiredviews, andgetattr-based dispatch, which can make it stop reporting unused functions.
We ran all three on the same Flask and Django apps. On Flask each found 5 of the 6 dead items; with default settings Vulture also reported 9 pieces of live code as dead, deadcode 8 and Skylos 3. On Django, a configured Vulture found all 6 dead items and Skylos found 4. Checked 10 October 2026; details below.
Results on the same two apps
Both apps have hand-labelled ground truth. The Flask app has blueprint routes, a before_app_request hook, an error handler, a template filter, a Click command, getattr dispatch and a pytest fixture. The Django app (Django 5.2, manage.py check passes) has function and class-based views, a URLconf, a ModelAdmin, a signal receiver, middleware, a management command and a model method used only in a template.
With default settings:
| Flask: dead found (of 6) | Flask: live code flagged | Django: dead found (of 6) | Django: live code flagged | Exit code | |
|---|---|---|---|---|---|
deadcode 2.4.1, deadcode . | 5 | 8 | 6 | 25 | 0 |
Vulture 2.16, vulture . | 5 | 9 | 6 | 29 | 3 |
Skylos 4.47.1, skylos . | 5 | 3 | 4 | 13 | 0 |
With each tool's documented configuration:
| Config used | Flask: dead / live flagged | Django: dead / live flagged | |
|---|---|---|---|
| Vulture 2.16 | [tool.vulture] with decorator and name patterns, plus a whitelist file (2 lines Flask, 5 lines Django) | 5 / 0 | 6 / 0 |
| Skylos 4.47.1 | Flask: one entry-point decorator and one whitelist pattern. Django: three whitelisted names, a settings.py override and --select SKY-U001,SKY-U002,SKY-U004 | 5 / 0 | 4 / 0 |
We didn't build a deadcode config for this test. The Vulture configs, with before and after output, are in Vulture false positives: tested configs.
What the numbers hide:
- The Flask miss was the same for everyone:
_csv_row, a helper whose only caller is dead. Vulture and deadcode don't follow dead code transitively. Skylos does, but thegetattrdispatch elsewhere in the app kept the helper alive; with a dict of handlers instead, or with--no-grep-verify, Skylos reported it. On a simpler example Skylos reported a whole dead chain where Vulture and deadcode reported only the first function (see the dead code guide). - Dynamic dispatch can silence Skylos. In a variant of the Flask app where a route calls a handler that uses
getattr(self, f"do_{action}"), Skylos 4.47.1 reported no unused functions at all, including the three dead ones in other modules, and gave no warning. Unused imports, classes and variables were still reported. Vulture and deadcode don't have this failure mode; they report thegetattrtargets as unused instead. - Skylos's Flask false positives: a
@bp.before_app_requesthook that 4.47.1 doesn't recognise, and two functions called throughgetattr. Its Django false positives were mostly the nine settings insettings.py, plusapplicationinwsgi.py, alist_displaymethod, a template-only model method and a view'smodelattribute. - Skylos's Django misses: an unrouted class-based view and an unrouted view behind
@login_required. In 4.47.1, classes that subclass a Django view base and views with auth decorators are treated as used. - Vulture's and deadcode's false positives were the framework entry points: routes, hooks, error handlers, signal receivers, admin classes, middleware, management commands and settings. That's by design; both expect you to configure them.
Raw output for the Flask app is on the dead code detection guide; the Django output is on the Django dead code page.
Feature comparison
| deadcode | Vulture | Skylos | |
|---|---|---|---|
| Best for | A one-off cleanup with automatic edits | A dedicated, configurable dead-code check | Framework-heavy apps, and dead code plus security in one tool |
| Finds | Unused code across the project | Unused code across the project, unreachable code | Unused code across the project, transitive dead code, unreachable code (--quality) |
| Ranking findings | No | Confidence by code type (60%, 90%, 100%) | Confidence 0–100 with the evidence behind it |
| Framework handling | Ignore options you configure | Decorator and name patterns, whitelist file | Built in for Flask, FastAPI, Django, pytest and others; extend with config |
| Removes code | --fix (preview: --fix --dry) | No | skylos clean: imports and functions (preview: --dry-run) |
| Fails CI on findings | No, exits 0 | Yes, exits 3 | With --strict (exit 1) |
| Also checks | — | — | Security, secrets, dependencies, quality (opt-in flags) |
| Licence | AGPL-3.0 | MIT | Apache-2.0 |
Maintenance, licence and CI behaviour
| deadcode | Vulture | Skylos | |
|---|---|---|---|
| Latest release | 2.4.1, 9 Aug 2024 | 2.16, 25 Mar 2026 | 4.47.1, 8 Oct 2026 |
| Recent activity | Last push June 2026 | Commits in Sep 2026 | Released Oct 2026 |
| Licence | AGPL-3.0 | MIT | Apache-2.0 |
| Exit code when it finds dead code | 0 (#35) | 3 (exit codes) | 0 with the default table, --format json or --format pretty; 1 with --format concise; 1 with --strict in any format; --gate alone: 0, as the default gate sets no dead-code limit |
| Open issues worth reading | Crash on Python 3.14 (#37); --fix breaks multiline assignments (#26) and from … import lines (#24); the decorator ignore option still flags functions (#30) | A false positive at 100% confidence (#422) | Skylos issues |
None of this makes a tool unusable. It tells you what to check before making one a required CI step.
Where deadcode is stronger
- It edits for you.
deadcode . --fix --drypreviews and--fixapplies, for functions, classes, variables and imports. Skylos'scleancovers only imports and functions; Vulture doesn't edit. Configure it first and read the preview: on our unconfigured Flask app, the preview deleted seven live functions (both routes, the request hook, the error handler, the template filter and the twogetattrhandlers) and rewroteimport osasimport, which is a syntax error. - Fine-grained ignore rules. It can ignore names, names in matching files, and names by decorator or base class, in
pyproject.toml. - Slightly fewer false positives than Vulture with defaults on both of our apps (8 vs 9, 25 vs 29), while finding the same dead code.
Where Vulture is stronger
- It's maintained and permissively licensed. MIT, a release in March 2026 and commits in September 2026.
- It fails CI without wrapping. Exit code 3 on findings.
- Configured, it was the most complete on Django. It found all 6 dead items, including the class-based view and the
@login_requiredview Skylos missed. - It's simple to reason about. Defined but never used, minus your whitelist. Its README is honest about what it can't see.
Where Skylos is stronger
- Less configuration on framework code. With defaults it flagged 3 live items on Flask against 8 and 9, and its Flask config was two entries.
- Transitive dead code. If a dead function is the only caller of other functions, Skylos can report the whole chain in one pass (with the caveat above).
- Evidence per finding. Each finding has a confidence score and the reasons behind it, which makes
--confidence 80a useful first pass. On Flask it removed the twogetattrfalse positives and kept every true finding. - Preview, then apply removals with
skylos clean . --dry-runand--apply. - More than dead code when you need it.
skylos . --danger --secretsadds security checks to the same run, andskylos cicd init --no-uploadwrites a pull-request workflow that needs no account.
What Skylos does not do: it doesn't parse Jinja or Django templates (including {% url %} tags), doesn't report Python modules that nothing imports, and skylos clean doesn't remove classes or variables.
Other Vulture alternatives
- Ruff and Flake8. Ruff's F401 and F841 work one file at a time, so they don't find a function nothing calls. The request for Vulture-like detection, ruff#872, has been open since November 2022; Astral's type checker ty has a "Dead code detection" request labelled
wish(ty#2829). More in Does Ruff detect dead code? - vulture-rs. A Rust reimplementation that describes its output as identical to Vulture 2.13; 1.0.0 was released in July 2026 under MIT (PyPI). Same model, so the same configuration needs.
- chokkin. A new tool (first release June 2026, MIT) that describes itself as "a Knip-like experience for Python": unused files, dependencies and public symbols (PyPI). See Is there a Knip for Python?
- GitHub Code Quality. Its CodeQL queries cover some per-file unused code; see GitHub Code Quality vs Skylos.
- coverage.py. Runtime evidence of which lines ran. Untested isn't the same as dead, so pair it with a static scan.
Does Skylos hold up on real projects?
10 Skylos-assisted dead-code cleanup PRs were merged into the default branches of 9 open-source projects:
| Project | Pull request | Merged |
|---|---|---|
| Black | psf/black#5041: Remove unused internal code | 2026-03-11 |
| Black | psf/black#5052: Code Cleanup: removed dead code identified by Skylos | 2026-03-29 |
| pypdf | py-pdf/pypdf#3685: DEV: Remove unused reverse encoding dicts | 2026-03-16 |
| mitmproxy | mitmproxy/mitmproxy#8136: cleanup: removed unused functions/classes and scripts | 2026-03-18 |
| NetworkX | networkx/networkx#8572: MAINT: remove unused functions and imports | 2026-03-18 |
| Optuna | optuna/optuna#6547: Remove dead code: unused functions, methods, and variables | 2026-03-23 |
| beets | beetbox/beets#6473: Remove dead code identified by static analysis | 2026-03-30 |
| PDM | pdm-project/pdm#3774: Remove dead code | 2026-04-30 |
| isort | PyCQA/isort#2525: Remove unused _ENCODING_PATTERN regex and re import | 2026-04-30 |
| react-error-boundary (TypeScript) | bvaughn/react-error-boundary#243: Remove unused docs and integration helpers | 2026-06-20 |
We opened 26 cleanup PRs in 22 projects. Of the other 16, most were closed; one was merged and then reverted, one was merged into a branch that never reached main, two are still open, and one merged PR fixed mutable default arguments rather than dead code. For Black, we sent the PRs and Black's maintainers reviewed and merged them. Candidates came from Skylos scans, many also triaged with skylos agent verify (an LLM review on top of the static scan), and each change was checked by hand. A merged PR is not an endorsement or adoption of Skylos, and the list is not an accuracy benchmark. The full story, misses included: dead-code cleanup PRs that maintainers merged.
Which should you pick?
- CI must fail on findings with no wrapper: Vulture (exit 3) or Skylos with
--strict. deadcode exits 0. - Django app, and you can maintain a config: Vulture with the tested Django config. If you use Skylos, check class-based views and decorated views by hand.
- Flask, FastAPI or pytest-heavy app, little time for config: Skylos, then add
[[tool.skylos.dead_code.entrypoints]]for hooks it misses. - You want the edits made: deadcode
--fix, orskylos clean --dry-runthen--applyfor imports and functions. Review the diff either way. - Licence constraints: deadcode is AGPL-3.0; Vulture is MIT; Skylos is Apache-2.0.
- Python 3.14: deadcode has an open crash report (#37).
- Security checks in the same run: Skylos.
All three install from PyPI and run without configuration, so the fastest way to decide is to run them on your repository:
pip install deadcode vulture skylos
deadcode .
vulture .
skylos .
Count the findings you would act on and the time you'd spend suppressing the rest.
Related
- Python dead code detection: find and remove unused code
- Vulture false positives: tested configs for Flask, Django and pytest
- Django dead code: find unused views, helpers and methods
- Does Ruff detect dead code?
- Is there a Knip for Python?
- Dead-code cleanup PRs that maintainers merged
- Bandit vs Skylos (if you also need security scanning)