Django: Clean up unused code with Vulture

Mucking out a code base.

As projects evolve, old functionality gets removed. Often such deletions are incomplete, leaving in their wake unused functions, classes, and other code objects. Unused code is clutter that brings no joy: it hinders comprehension, taxes codebase-wide quality improvements, and can sometimes lead to errors if later used.

In an ideal world, test coverage data would reveal unused code, since a perfect test suite has 100% coverage of all features. But most projects don’t live in the ideal world, so unused code detectors exist. These tools use static analysis and heuristics to find probably-unused code, which you can then decide whether to remove.

Such tools cannot be certain because Python makes it impossible to be sure that an object isn’t used. Objects can always be referenced with dynamically-constructed names, such as through globals():

def dynamic_about(request):
    page = request.GET.get("page", "company")
    if page not in {"company", "team", "..."}:
        page = "company"
    return globals()[f"about_{page}"](request)


def about_company(request):
    ...

Normally projects don’t have much dynamic dispatch like this, but it’s always a possibility. That’s why you need to review findings from an unused code detector before acting on it.

Enter the Vulture

Vulture is a popular unused code detector for Python. It analyzes the Abstract Syntax Trees (ASTs) of given Python files and uses those to form a list of all class, function, and module-level object names. If any name doesn’t seem to have a corresponding use, Vulture reports the name as unused, along with an approximate confidence level.

Vulture can be a bit zealous, especially with a framework like Django that often calls your project’s code implicitly. To counter this, it’s worth using Vulture’s configuration to ignore some files, object names, and functions with certain decorators.

Below is a basic Django-oriented configuration for Vulture in pyproject.toml. This is based on my experience running Vulture on a couple of medium-large Django projects.

[tool.vulture]
# Configuration for vulture: https://github.com/jendrikseipp/vulture
# Install in your virtual environment and run:
# python -m vulture | tail -r | less
# The below configuration tries to remove some false positives, but there are
# still many, for example for model properties used only in templates.
# See also:
# https://adamj.eu/tech/2023/07/12/django-clean-up-unused-code-vulture/
exclude = [
  "*/settings.py",
  "*/settings/*.py",
  "*/migrations/*.py",
]
ignore_decorators = [
  # Django
  "@receiver",
  "@register.filter",
  "@register.inclusion_tag",
  "@register.simple_tag",
  # django.contrib.admin
  "@admin.action",
  "@admin.display",
  "@admin.register",
  # pytest
  "@pytest.fixture",
]
ignore_names = [
  # Django
  "*Config",  # AppConfig subclasses
  "*Middleware",
  "clean_*",
  "Meta",
  "urlpatterns",
  # django.contrib.admin
  "get_extra",
  "get_fieldsets",
  "has_add_permission",
  "has_change_permission",
  "has_delete_permission",
  "has_view_permission",
  "lookups",
]
paths = [
  "example",
]
sort_by_size = true

The exclude, ignore_decorators, and ignore_names settings all remove false positives. They aren’t intended to be exhaustive lists, they’re only what I found sufficient to make the results usable. Extend them as necessary, for example, if you use Factory Boy you will probably want @factory.post_generation in ignore_decorators.

To run Vulture with this configuration:

  1. Install Vulture.

    $ python -m pip install vulture
    
  2. Add the configuration to your pyproject.toml.

  3. Adjust paths to contain the list of your project’s top-level modules.

  4. Run Vulture. I use:

    $ python -m vulture | tail -r | less
    
    • tail -r reverses the order, so the largest objects appear first. Vulture’s sort_by_size option sorts in size-ascending order, but they are more useful in size-descending order.
    • less paginates the output, which is a necessity with large numbers of potentially-unused objects.

The output looks like:

$ python -m vulture | tail -r | less
example/views.py:23: unused function 'old_dashboard' (60% confidence, 17 lines)
example/views.py:20: unused variable 'DASHBOARD_WIDGET_COUNT' (60% confidence, 1 line)
example/urls.py:5: unused variable 'old_dashboard_urlpatterns' (60% confidence, 1 line)

With a fancy terminal, the <filename>:<lineno> prefixes act as links into the code. For example, iTerm allows you to open the links with command-click. Then you can investigate and maybe delete.

Before removing something, I recommend at least these two steps:

  1. Grep for usage, especially in templates which Vulture doesn’t check. I like ripgrep for this:

    $ rg old_dashboard
    example/views.py
    23:def old_dashboard(request):
    
    example/urls.py
    5:old_dashboard_urlpatterns = [
    

    I cover ripgrep in my new book about Git.

  2. Check the Git log for when the last usage was removed, or why an object exists in the first place. git log -S filters the log to commits that added or removed a given string:

    $ git log -S old_dashboard
    commit 17ce51e39db2780ea6ce95943fb9420bbb38c775
    Author: A Hacker <...>
    Date:   ...
    
        Swap routing from old to new dashboard
    ...
    

    I covered git log -S in more detail in my previous post How to clean up unused code with Git.

If your short investigation gives you confidence that the target object is unused, go ahead and delete it, then pat yourself on the back. (If you were wrong, the test suite will catch it, hopefully.)

Possible false positives

There are some kinds of Djangoey objects that Vulture definitely won’t detect usage of:

Counter such false positives by always grepping for usage and adding names to your Vulture configuration.

Allow-listing names

Vulture also supports generating and reading an “allow list module” to flag names as used. This is a fake module that imports and accesses code objects so they count as used. (Currently, the feature uses the term “whitelist” but this is set to change.)

I haven’t used this feature because I haven’t been concerned with clearing all false positives. But it looks useful if you plan to run Vulture regularly. The only risk is the fake module getting out of date, leading to false negatives.

Fin

I’ve had success using Vulture on a couple of Django projects now. I just completed a large cleanup for my client Silvr: 100 code objects removed across 9 pull requests, totalling 1671 lines. That certainly helps with ongoing maintenance.

May you lighten every code base you touch,

—Adam


Newly updated: my book Boost Your Django DX now covers Django 5.0 and Python 3.12.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: