Automatically upgrade your Django projects.
Use pip:
python -m pip install django-upgrade
Python 3.8 to 3.11 supported.
Or with pre-commit in the repos
section of your .pre-commit-config.yaml
file (docs):
- repo: https://github.com/adamchainz/django-upgrade
rev: '' # replace with latest tag on GitHub
hooks:
- id: django-upgrade
args: [--target-version, "3.2"] # Replace with Django version
Want to improve your code quality? Check out my book Boost Your Django DX which covers using pre-commit, django-upgrade, and many other tools. I wrote django-upgrade whilst working on the book!
django-upgrade
is a commandline tool that rewrites files in place.
Pass your Django version as <major>.<minor>
to the --target-version
flag.
The built-in fixers will rewrite your code to avoid some DeprecationWarning
s and use some new features on your Django version.
For example:
django-upgrade --target-version 3.2 example/core/models.py example/settings.py
The --target-version
flag defaults to 2.2, the oldest supported version when this project was created.
For more on usage run django-upgrade --help
.
django-upgrade
focuses on upgrading your code and not on making it look nice.
Run django-upgrade before formatters like Black.
django-upgrade
does not have any ability to recurse through directories.
Use the pre-commit integration, globbing, or another technique for applying to many files such as with git ls-files | xargs
.
The full list of fixers is documented below.
django-codemod is a pre-existing, more complete Django auto-upgrade tool, written by Bruno Alla. Unfortunately its underlying library LibCST is particularly slow, making it annoying to run django-codemod on every commit and in CI. Additionally LibCST only advertises support up to Python 3.8 (at time of writing).
django-upgrade is an experiment in reimplementing such a tool using the same techniques as the fantastic pyupgrade. The tool leans on the standard library’s ast and tokenize modules, the latter via the tokenize-rt wrapper. This means it will always be fast and support the latest versions of Python.
For a quick benchmark: running django-codemod against a medium Django repository with 153k lines of Python takes 133 seconds. pyupgrade and django-upgrade both take less than 0.5 seconds.
Add on_delete=models.CASCADE
to ForeignKey
and OneToOneField
:
-models.ForeignKey("auth.User")
+models.ForeignKey("auth.User", on_delete=models.CASCADE)
-models.OneToOneField("auth.User")
+models.OneToOneField("auth.User", on_delete=models.CASCADE)
Rewrites some compatibility imports:
django.forms.utils.pretty_name
indjango.forms.forms
django.forms.boundfield.BoundField
indjango.forms.forms
Whilst mentioned in the Django 3.1 release notes, these have been possible since Django 1.9.
-from django.forms.forms import pretty_name
+from django.forms.utils import pretty_name
Rewrites some compatibility imports:
django.core.exceptions.EmptyResultSet
indjango.db.models.query
,django.db.models.sql
, anddjango.db.models.sql.datastructures
django.core.exceptions.FieldDoesNotExist
indjango.db.models.fields
Whilst mentioned in the Django 3.1 release notes, these have been possible since Django 1.11.
-from django.db.models.query import EmptyResultSet
+from django.core.exceptions import EmptyResultSet
-from django.db.models.fields import FieldDoesNotExist
+from django.core.exceptions import FieldDoesNotExist
Rewrites imports of include()
and url()
from django.conf.urls
to django.urls
.
url()
calls using compatible regexes are rewritten to the new path()
syntax, otherwise they are converted to call re_path()
.
-from django.conf.urls import include, url
+from django.urls import include, path, re_path
urlpatterns = [
- url(r'^$', views.index, name='index'),
+ path('', views.index, name='index'),
- url(r'^about/$', views.about, name='about'),
+ path('about/', views.about, name='about'),
- url(r'^post/(?P<slug>[-a-zA-Z0-9_]+)/$', views.post, name='post'),
+ re_path(r'^post/(?P<slug>[w-]+)/$', views.post, name='post'),
- url(r'^weblog/', include('blog.urls')),
+ path('weblog/', include('blog.urls')),
]
Rewrites imports of lru_cache
from django.utils.functional
to use functools
.
-from django.utils.functional import lru_cache
+from functools import lru_cache
Rewrites use of request.META
to read HTTP headers to instead use request.headers
.
-request.META['HTTP_ACCEPT_ENCODING']
+request.headers['Accept-Encoding']
-self.request.META.get('HTTP_SERVER', '')
+self.request.headers.get('Server', '')
Rewrites deprecated alias django.core.paginator.QuerySetPaginator
to Paginator
.
-from django.core.paginator import QuerySetPaginator
+from django.core.paginator import Paginator
-QuerySetPaginator(...)
+Paginator(...)
Rewrites deprecated class FixedOffset(x, y))
to timezone(timedelta(minutes=x), y)
Known limitation: this fixer will leave code broken with an ImportError
if FixedOffset
is called with only *args
or **kwargs
.
-from django.utils.timezone import FixedOffset
-FixedOffset(120, "Super time")
+from datetime import timedelta, timezone
+timezone(timedelta(minutes=120), "Super time")
Rewrites model and form fields using FloatRangeField
to DecimalRangeField
, from the relevant django.contrib.postgres
modules.
from django.db.models import Model
-from django.contrib.postgres.fields import FloatRangeField
+from django.contrib.postgres.fields import DecimalRangeField
class MyModel(Model):
- my_field = FloatRangeField("My range of numbers")
+ my_field = DecimalRangeField("My range of numbers")
Rewrites the allow_database_queries
and multi_db
attributes of Django’s TestCase
classes to the new databases
attribute.
This only applies in test files, which are heuristically detected as files with either “test” or “tests” somewhere in their path.
Note that this will only rewrite to databases = []
or databases = "__all__"
.
With multiple databases you can save some test time by limiting test cases to the databases they require (which is why Django made the change).
from django.test import SimpleTestCase
class MyTests(SimpleTestCase):
- allow_database_queries = True
+ databases = "__all__"
def test_something(self):
self.assertEqual(2 * 2, 4)
Rewrites smart_text()
to smart_str()
, and force_text()
to force_str()
.
-from django.utils.encoding import force_text, smart_text
+from django.utils.encoding import force_str, smart_str
-force_text("yada")
-smart_text("yada")
+force_str("yada")
+smart_str("yada")
Rewrites the urlquote()
, urlquote_plus()
, urlunquote()
, and urlunquote_plus()
functions to the urllib.parse
versions.
Also rewrites the internal function is_safe_url()
to url_has_allowed_host_and_scheme()
.
-from django.utils.http import urlquote
+from urllib.parse import quote
-escaped_query_string = urlquote(query_string)
+escaped_query_string = quote(query_string)
Rewrites unescape_entities()
with the standard library html.escape()
.
-from django.utils.text import unescape_entities
+import html
-unescape_entities("some input string")
+html.escape("some input string")
Rewrites the ugettext()
, ugettext_lazy()
, ugettext_noop()
, ungettext()
, and ungettext_lazy()
functions to their non-u-prefixed versions.
-from django.utils.translation import ugettext as _, ungettext
+from django.utils.translation import gettext as _, ngettext
-ungettext("octopus", "octopodes", n)
+ngettext("octopus", "octopodes", n)
Rewrites imports of JSONField
and related transform classes from those in django.contrib.postgres
to the new all-database versions.
Ignores usage in migration files, since Django kept the old class around to support old migrations.
You will need to make migrations after this fix makes changes to models.
-from django.contrib.postgres.fields import JSONField
+from django.db.models import JSONField
Rewrites the setting PASSWORD_RESET_TIMEOUT_DAYS
to PASSWORD_RESET_TIMEOUT
, adding the multiplication by the number of seconds in a day.
Settings files are heuristically detected as modules with the whole word “settings” somewhere in their path.
For example myproject/settings.py
or myproject/settings/production.py
.
-PASSWORD_RESET_TIMEOUT_DAYS = 4
+PASSWORD_RESET_TIMEOUT = 60 * 60 * 24 * 4
Removes the deprecated documentation-only providing_args
argument.
from django.dispatch import Signal
-my_cool_signal = Signal(providing_args=["documented", "arg"])
+my_cool_signal = Signal()
Injects the now-required length
argument, with its previous default 12
.
from django.utils.crypto import get_random_string
-key = get_random_string(allowed_chars="01234567899abcdef")
+key = get_random_string(length=12, allowed_chars="01234567899abcdef")
Transforms the NullBooleanField()
model field to BooleanField(null=True)
.
Ignores usage in migration files, since Django kept the old class around to support old migrations.
You will need to make migrations after this fix makes changes to models.
-from django.db.models import Model, NullBooleanField
+from django.db.models import Model, BooleanField
class Book(Model):
- valuable = NullBooleanField("Valuable")
+ valuable = BooleanField("Valuable", null=True)
Rewrites keyword arguments to their new names: whitelist
to allowlist
, and domain_whitelist
to domain_allowlist
.
from django.core.validators import EmailValidator
-EmailValidator(whitelist=["example.com"])
+EmailValidator(allowlist=["example.com"])
-EmailValidator(domain_whitelist=["example.org"])
+EmailValidator(domain_allowlist=["example.org"])
Removes module-level default_app_config
assignments from __init__.py
files:
-default_app_config = 'my_app.apps.AppConfig'
There are no fixers for Django 4.0 at current. Most of its deprecations don’t seem automatically fixable.