Django calls a lot of your code without your code ever calling it. Views are listed in urlpatterns. Signal receivers are wired by a decorator. Admin methods are named as strings in list_display. Middleware and context processors are dotted paths in settings.py. Management commands are found by file name. Templates call model methods.
A dead-code tool that doesn't know this reports half of a Django app as unused. A tool that trusts it too much hides real dead code. This page shows what Skylos 4.47.1 does on a sample Django and DRF project: what it keeps live, what it finds, which false positives you'll see, and what it misses.
What Skylos treats as used
| Django convention | How Skylos treats it |
|---|---|
Function view referenced in any urlpatterns (path, re_path) | Used |
Class-based view used with .as_view(), and its get/post/... methods | Used |
CBV hooks such as get_queryset, get_context_data, form_valid on a class that directly subclasses a Django view | Used |
@receiver(...) handlers and functions passed to signal.connect(...) | Used |
ModelAdmin hooks, option attributes (list_display, actions, ...), functions listed in actions=[...], admin.site.register(Model, Admin) | Used |
Model hooks (save, clean, get_absolute_url, natural_key, ...), Meta, fields | Used |
Form clean() and clean_<field>() | Used |
Management command Command, handle, add_arguments | Used |
MiddlewareMixin subclasses and their process_* methods | Used |
Template tags and filters registered with @register.filter, simple_tag, inclusion_tag, tag | Used |
Migration RunPython callbacks | Used |
Celery @shared_task / @app.task | Used |
DRF viewset actions (list, create, retrieve, ...), serializer validate, validate_<field>, to_representation, get_<field> | Used |
The base-class checks look at the direct base. If your view, model or admin inherits from your own base class (a project BaseAdminView, an abstract model), some of that handling no longer applies and you'll see more findings on that class.
What it finds
With the config from the next section, a first pass on the sample project at confidence 80, limited to unused functions and classes, lists only code that really is unused:
$ skylos . --confidence 80 --select SKY-U001,SKY-U004 --format concise
shop/admin.py:16 SKY-U001 unused function: unused_admin_action
shop/context_processors.py:5 SKY-U001 unused function: unused_processor
shop/migrations/0001_initial.py:12 SKY-U001 unused function: unused_migration_helper
shop/signals.py:19 SKY-U001 unused function: recalculate_everything
shop/tasks.py:9 SKY-U001 unused function: unused_task_helper
shop/templatetags/shop_tags.py:21 SKY-U001 unused function: unused_tag_helper
shop/views.py:28 SKY-U001 unused function: old_landing
shop/views.py:46 SKY-U001 unused function: unused_helper
shop/middleware.py:25 SKY-U004 unused class: UnusedMiddleware
shop/views.py:91 SKY-U004 unused class: PlainHelperClass
old_landing is a function view that no URLconf references. At the default threshold (60), Skylos also lists unused methods on models, querysets, views, viewsets, serializers, forms, admin classes and commands, such as Product.price_with_tax or ProductViewSet.compute_discount. Those are worth reviewing, but check templates first: Skylos doesn't read them, so a model method used only in a template looks unused.
False positives you will see, and the fix
Skylos doesn't read the strings in settings.py (INSTALLED_APPS, MIDDLEWARE, TEMPLATES), so anything Django loads only from a dotted path looks unused. Without config, the sample project's first scan reported:
- every variable in
settings.py, andapplicationinwsgi.py; - a plain middleware class listed only in
MIDDLEWARE; - a context processor listed only in
TEMPLATES; - the function named by
handler404 = "shop.views.not_found"; - the
TestCaseclass in the app'stests.py(Skylos treats files undertests/ortest/and*_test.pyas tests, not a file calledtests.py); - methods named only as strings in
list_display,@admin.displaymethods and DRF@actionmethods; - unused
request,modeladminandquerysetparameters (SKY-U006), class attributes Django reads such aspaginate_by, and declared form fields (SKY-U003).
This block in pyproject.toml fixed the first five groups and the decorated methods on the sample:
[tool.skylos.whitelist]
names = [
"application", # wsgi.py / asgi.py, loaded by the server
"TimingMiddleware", # listed in settings.MIDDLEWARE
"process_view", # Django middleware hook
"shop_settings", # listed in TEMPLATES context_processors
"not_found", # handler404 = "shop.views.not_found"
]
[tool.skylos.overrides."settings.py"]
whitelist = ["*"]
[tool.skylos.overrides."tests.py"]
whitelist = ["*"]
[[tool.skylos.dead_code.entrypoints]]
type = "method"
decorators = ["admin.display", "action"]
reason = "Django admin display methods and DRF @action routes"
For the rest:
- Parameters and class attributes: leave them out of the first pass with
--select SKY-U001,SKY-U004, as above. - One-off cases: put
# skylos: ignoreon thedefline. - Methods named only in
list_displaystrings: add the name to the whitelist.
Keep the TOML valid. In 4.47.1, if pyproject.toml doesn't parse, Skylos ignores the whole [tool.skylos] section without a warning, and every whitelisted name comes back. One way to check: python -c "import tomllib; tomllib.load(open('pyproject.toml', 'rb'))".
Avoid path in entry-point rules unless you need it, and check the output after adding one: on the sample, a path rule for middleware.py also hid a middleware class that really was unused.
What it doesn't find
Be clear about the blind spots before you trust a clean result:
- Unused class-based views, DRF views and serializers, forms, ModelAdmins and models that directly subclass a Django or DRF base are never reported, even when nothing uses them. Their unused methods are reported; the classes are not.
- Views behind certain decorators. Unused function views decorated with
@login_required,@permission_required,@staff_member_required,@require_POST,@require_GETor@api_vieware treated as used. On the sample, two such views were missed even at confidence 0. - Dead URLconfs. Skylos doesn't follow
include("app.urls")strings. A view listed in a URLconf that nothing includes still counts as used. - Receivers that never connect. A
@receiverin a module that is never imported (for example, a missing import inAppConfig.ready()) still counts as used. - Templates. Template code isn't scanned, in either direction.
So use Skylos to find unused function views, helpers, admin actions, tasks, template-tag helpers and unused methods. For unused routes, use runtime evidence: access logs or request metrics show which URLs nobody calls, which no static tool can tell you.
A workflow that holds up
- First pass:
skylos . --confidence 80 --select SKY-U001,SKY-U004. - Teach it your string-loaded names with the config above.
- Review the 60% method findings, checking templates and
getattr-style dynamic use. - Preview removals:
skylos clean . --dry-runshows the unused imports and functions it would delete;--applywrites them. - Stop new dead code with a pull-request check:
skylos cicd init --no-uploadwrites a GitHub Actions workflow that needs no Skylos account or API key.
The dead-code docs cover confidence levels and suppressions. For the security side of a Django app, see Django security scanning: what static analysis catches.
Related
- How to detect dead code in Python
- Does Ruff detect dead code?
- deadcode vs Vulture vs Skylos
- Dead-code cleanup PRs that maintainers merged
Try it
pip install skylos
skylos . --confidence 80 --select SKY-U001,SKY-U004