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_request hooks, unused Django class-based views and @login_required views, and getattr-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 flaggedDjango: dead found (of 6)Django: live code flaggedExit code
deadcode 2.4.1, deadcode .586250
Vulture 2.16, vulture .596293
Skylos 4.47.1, skylos .534130

With each tool's documented configuration:

Config usedFlask: dead / live flaggedDjango: dead / live flagged
Vulture 2.16[tool.vulture] with decorator and name patterns, plus a whitelist file (2 lines Flask, 5 lines Django)5 / 06 / 0
Skylos 4.47.1Flask: one entry-point decorator and one whitelist pattern. Django: three whitelisted names, a settings.py override and --select SKY-U001,SKY-U002,SKY-U0045 / 04 / 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 the getattr dispatch 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 the getattr targets as unused instead.
  • Skylos's Flask false positives: a @bp.before_app_request hook that 4.47.1 doesn't recognise, and two functions called through getattr. Its Django false positives were mostly the nine settings in settings.py, plus application in wsgi.py, a list_display method, a template-only model method and a view's model attribute.
  • 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

deadcodeVultureSkylos
Best forA one-off cleanup with automatic editsA dedicated, configurable dead-code checkFramework-heavy apps, and dead code plus security in one tool
FindsUnused code across the projectUnused code across the project, unreachable codeUnused code across the project, transitive dead code, unreachable code (--quality)
Ranking findingsNoConfidence by code type (60%, 90%, 100%)Confidence 0–100 with the evidence behind it
Framework handlingIgnore options you configureDecorator and name patterns, whitelist fileBuilt in for Flask, FastAPI, Django, pytest and others; extend with config
Removes code--fix (preview: --fix --dry)Noskylos clean: imports and functions (preview: --dry-run)
Fails CI on findingsNo, exits 0Yes, exits 3With --strict (exit 1)
Also checks——Security, secrets, dependencies, quality (opt-in flags)
LicenceAGPL-3.0MITApache-2.0

Maintenance, licence and CI behaviour

deadcodeVultureSkylos
Latest release2.4.1, 9 Aug 20242.16, 25 Mar 20264.47.1, 8 Oct 2026
Recent activityLast push June 2026Commits in Sep 2026Released Oct 2026
LicenceAGPL-3.0MITApache-2.0
Exit code when it finds dead code0 (#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 readingCrash 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 --dry previews and --fix applies, for functions, classes, variables and imports. Skylos's clean covers 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 two getattr handlers) and rewrote import os as import , 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_required view 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 80 a useful first pass. On Flask it removed the two getattr false positives and kept every true finding.
  • Preview, then apply removals with skylos clean . --dry-run and --apply.
  • More than dead code when you need it. skylos . --danger --secrets adds security checks to the same run, and skylos cicd init --no-upload writes 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:

ProjectPull requestMerged
Blackpsf/black#5041: Remove unused internal code2026-03-11
Blackpsf/black#5052: Code Cleanup: removed dead code identified by Skylos2026-03-29
pypdfpy-pdf/pypdf#3685: DEV: Remove unused reverse encoding dicts2026-03-16
mitmproxymitmproxy/mitmproxy#8136: cleanup: removed unused functions/classes and scripts2026-03-18
NetworkXnetworkx/networkx#8572: MAINT: remove unused functions and imports2026-03-18
Optunaoptuna/optuna#6547: Remove dead code: unused functions, methods, and variables2026-03-23
beetsbeetbox/beets#6473: Remove dead code identified by static analysis2026-03-30
PDMpdm-project/pdm#3774: Remove dead code2026-04-30
isortPyCQA/isort#2525: Remove unused _ENCODING_PATTERN regex and re import2026-04-30
react-error-boundary (TypeScript)bvaughn/react-error-boundary#243: Remove unused docs and integration helpers2026-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, or skylos clean --dry-run then --apply for 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.