# django-upgrade is a code rewriter you can hang off a pre-commit hook

> django-upgrade automates the source changes a Django project needs when it moves to a newer Django or Python version, and the repository shows a tool built for automation rather than for interactive use: a single runtime dependency on a token-level rewriting library, a published pre-commit hook, a uv build backend with a lockfile, and a coverage badge that claims one hundred percent.

**adamchainz/django-upgrade** — Automatically upgrade your Django projects.

- Repository: https://github.com/adamchainz/django-upgrade
- Website: https://django-upgrade.readthedocs.io/en/latest/
- Stars: 1,252 · Forks: 82
- Language: Python
- License: MIT
- Published: 2026-10-17 · Updated: 2026-10-17 · Language: en
- Canonical page: https://hysenlabs.com/projects/adamchainz-django-upgrade

## The runtime dependency list is one line

The shortest honest description of this project is in its packaging metadata.

```toml
[project]
name = "django-upgrade"
version = "1.33.0"
requires-python = ">=3.11"
dependencies = [
  "tokenize-rt>=4.1",
]
scripts.django-upgrade = "django_upgrade.main:main"
```

One runtime dependency, and it is a token-level rewriting library rather than an AST or framework package. That single choice carries most of the design.

A tool that rewrites source code has three ways to do it badly. It can use regular expressions, which match inside string literals and comments and produce code that parses and behaves differently. It can use an abstract syntax tree in a way that loses formatting and comments on output. It can work on the token stream, where each string, name, operator and comment is a distinct item with its original spelling and position.

Choosing `tokenize-rt` puts this project in the third category. Formatting survives, comments survive, and a pattern that happens to match inside a docstring does not trigger a rewrite. For a tool whose entire job is editing source files you did not write, that is the difference between a refactor and an incident.

It also means the tool does not need to import Django to know anything about Django. There is no dependency on Django itself in the runtime requirements, which is why the package can be installed and run on a project whose Django version you are trying to move away from.

The command line entry point is a single console script named `django-upgrade`, wired to `django_upgrade.main:main`.

## Django 2.2 to 6.0 classifiers against a Python 3.11 floor

The version metadata is the most informative part of the file after the dependency list, because the two ranges tell different stories.

The Python requirement is `>=3.11`, and the classifiers list 3.11 through 3.15. The Django classifiers, however, run from 2.2 all the way to 6.0: 2.2, 3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 5.0, 5.1, 5.2 and 6.0.

Those are not contradictory. They describe a tool whose output targets old Django releases and whose own execution requires a modern Python. That is exactly the upgrade path this kind of project exists for: the code being rewritten is Django 2.2 era, and the machine running the rewrite is on a current Python.

The classifiers also list Intended Audience as Developers, Operating System as Independent, Programming Language as Python 3 Only, and Typing as Typed. That last one is worth noting, because a source rewriter that ships type information is built by someone who intends it to be used in a typed codebase, where an imprecise rewrite would be caught by the type checker rather than in production.

The development status classifier is 5 - Production/Stable, and the project is at version 1.33.0. There are no GitHub releases, so the version history is on PyPI and in the repository rather than in release tags.

## It ships as a pre-commit hook, not just a command

Two files at the repository root decide how the tool is meant to be used.

There is a `.pre-commit-config.yaml` and a `.pre-commit-hooks.yaml`. The second is the manifest that tells pre-commit what the hook is called and how to invoke it, which means this project is designed to be consumed by pre-commit out of the box rather than by wiring a script into a Makefile yourself.

That choice changes the shape of an upgrade. Running django-upgrade once across a codebase produces a large commit that nobody can review. Running it as a pre-commit hook means the rewrite happens on files as they are touched, so the diff for each change is small, lands with the change that prompted it, and can be reverted with it.

For a project that has been sitting on an old Django version for years, that is the difference between an upgrade that happens and an upgrade that is postponed indefinitely. There is a badge in the README confirming the pre-commit integration is enabled.

The other pre-commit-adjacent file at the root is `.editorconfig`, alongside `.typos.toml`, which configures a spell checker. Both are small signals that the repository treats its own source as something a contributor should not have to think about formatting on.

The build and documentation tooling is worth one more note. The build backend is `uv_build` with a version constraint on `uv-build`, the tests and docs are declared as dependency groups, and `uv.lock` sits at the root, so the whole project is built, tested and documented through uv with a locked dependency set.

## Coverage, tests and two history files

The badges in the README and the dependency groups in pyproject describe the same project from two sides.

There is a coverage badge claiming one hundred percent, with a link to the continuous integration run. A codemod that rewrites source code can claim that honestly only because the logic is small and branch-complete: there is one behaviour per rewrite rule, and a test asserts the before and after text of each. There is no I/O to mock, no database and no framework to configure.

The test group lists `coverage[toml]`, `pytest>=9` and `pytest-randomly`. That last one is a deliberate choice rather than a default: randomising test order catches tests that depend on each other's state, which in a project whose inputs are source strings is a real failure mode if any global caches exist.

Two history files sit at the root, `CHANGELOG.rst` and `HISTORY.rst`. Having both is unusual and suggests they serve different readers: a changelog for what changed in each release, and a longer history that reaches further back for people looking for when a specific rewrite was introduced or removed.

The documentation group lists `furo`, `sphinx` and `sphinx-copybutton`, so the documentation site is a Sphinx project published to Read The Docs, with the Furo theme and a copy button for code samples.

The last push was on 2026-10-05, and the licence is MIT, authored by Adam Johnson.

## Conclusion

Adopt django-upgrade when you have a long-lived Django project with a deprecation backlog, since the work it removes is mechanical and the pre-commit hook lets you keep the project on the current idioms one commit at a time. Skip it if your codebase is small enough that a search and replace covers it. Verify first that your Python version meets the 3.11 floor of the current release, since running the tool is how you discover which rewrites it applies to your code.

## FAQ

### How do I install django-upgrade?

The package is on PyPI as `django-upgrade` at version 1.33.0 and installs a single console script named `django-upgrade`. Full usage instructions are on the Read the Docs site rather than in the README.

### Does django-upgrade need Django installed?

No. The runtime dependency list contains only `tokenize-rt>=4.1`, so the tool rewrites source without importing Django, which is what lets it work on a project you are trying to upgrade away from an old version.

### Can django-upgrade run as a pre-commit hook?

Yes. The repository ships `.pre-commit-hooks.yaml` as well as `.pre-commit-config.yaml`, so the hook is described for pre-commit and can be applied to files as they change rather than in one large commit.

## Sources

- [adamchainz/django-upgrade on GitHub](https://github.com/adamchainz/django-upgrade)
- [Issues](https://github.com/adamchainz/django-upgrade/issues)
- [License: MIT](https://github.com/adamchainz/django-upgrade/blob/main/LICENSE)
- [Project website](https://django-upgrade.readthedocs.io/en/latest/)
- [README](https://github.com/adamchainz/django-upgrade/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/adamchainz-django-upgrade
