Django: Fix version 5.0’s URLField.assume_scheme
warnings
Since Django’s inception, the web has gradually moved from HTTP to HTTPS, a welcome move for security. But the history has meant older parts of Django have had a lingering HTTP bias. Many of these have been migrated to default to HTTPS instead in previous versions. Django 5.0 starts the migration of another, tiny HTTP bias in forms.URLField
.
The old behaviour: when URLField
is provided a URL without a scheme, it assumes it to be “http”:
In [1]: from django import forms
In [2]: forms.URLField().to_python('example.com')
Out[2]: 'http://example.com'
Django 5.0 has started a deprecation process to change this default to “https” (Ticket #34380). This version shows a PendingDeprecationWarning
when instantiating a URLField
:
In [1]: from django import forms
In [2]: forms.URLField().to_python('example.com')
<ipython-...>:1: RemovedInDjango60Warning: The default scheme will be changed from 'http' to 'https' in Django 6.0. Pass the forms.URLField.assume_scheme argument to silence this warning.
forms.URLField().to_python('example.com')
Out[2]: 'http://example.com'
Here’s that warning message in a more readable format:
RemovedInDjango60Warning: The default scheme will be changed from 'http' to 'https' in Django 6.0. Pass the forms.URLField.assume_scheme argument to silence this warning.
Django 5.1 will turn that into a DeprecationWarning
and Django 6.0 will change the default and remove the warning.
Here’s the related release note:
The default scheme forforms.URLField
will change from"http"
to"https"
in Django 6.0. SetFORMS_URLFIELD_ASSUME_HTTPS
transitional setting toTrue
to opt into assuming"https"
during the Django 5.x release cycle.
Receiving a schemeless URL in a URLField
should actually be quite rare. By default, a URLField
renders as <input type=url>
:
In [1]: from django import forms
In [2]: class CheckForm(forms.Form):
...: url = forms.URLField(label="URL to check")
...:
In [3]: print(CheckForm()["url"].as_field_group())
<label for="id_url">URL to check:</label>
<input type="url" name="url" required id="id_url">
Browsers’ client-side validation for <input type=url>
requires a scheme for URLs. To hit the “assume scheme” code path, the field will need to use a different widget or template, or the client-side validation would need to be disabled.
But still, until Django 6.0, there’s this new warning to account for. You can control the warning in one of two ways:
- Adopt the future behaviour with the
FORMS_URLFIELD_ASSUME_HTTPS
transitional setting. - Migrate individual
forms.URLField
instances by adding theassume_scheme
argument.
We’ll cover both of those options below.
I think the first option is suitable for most projects, where there are likely few concerns around users entering schemeless HTTP URLs. Most links are copy-pasted from the browser, in which case they come with a scheme. For the rest, Google Chrome statistics show >93% of page loads use HTTPS. Also, if you go with the second option, you’ll need to deal with warnings for every forms.URLField
in your project which can be a lot of work.
1. Adopt the future behaviour with the transitional setting
Enable FORMS_URLFIELD_ASSUME_HTTPS
in your settings file to make all forms.URLField
instances assume “https”:
FORMS_URLFIELD_ASSUME_HTTPS = True
The setting is transitional, meaning it will be removed in Django 6.0. Because of this, enabling it triggers a different warning when Django starts:
RemovedInDjango60Warning: The FORMS_URLFIELD_ASSUME_HTTPS transitional setting is deprecated.
Silence that warning with one of Python’s warning control mechanisms, such as warnings.filterwarnings()
. It’s pretty easy to do so right next to the setting:
from warnings import filterwarnings
filterwarnings(
"ignore", "The FORMS_URLFIELD_ASSUME_HTTPS transitional setting is deprecated."
)
FORMS_URLFIELD_ASSUME_HTTPS = True
That’s all you need for this migration option!
2. Migrate individual forms.URLField
instances
For this option, you need to add the assume_scheme
argument to each URLField
instance. You can set it to "https"
to adopt the future default behaviour or "http"
to retain the old behaviour—any explicit definition silences the warning.
But passing the argument can be hard depending on how the form class is defined or created. Here are some different ways form classes may exist in your project.
Plain Form
s with a URLField
wrapper
For forms that you directly define, add the assume_scheme
argument to any URLField
instances:
from django import forms
class CheckForm(forms.Form):
url = forms.URLField(label="URL to check", assume_scheme="https")
If you have many URLField
s, try functools.partial
to reduce the repetition:
from functools import partial
from django import forms
HTTPSURLField = partial(forms.URLField, assume_scheme="https")
class CheckForm(forms.Form):
url = HTTPSURLField(label="URL to check")
(See my previous posts on using partial
in Django: three uses, and three more uses.)
ModelForm
classes with formfield_callback
ModelForm
classes are a bit more tricky since their fields are automatically created “behind the scenes” from model fields. You can override these with explicit form fields, but that loses the automatic synchronization of other field arguments, such as max_length
.
Instead, use formfield_callback
, added in Django 4.2. This callback can wrap the creation of form fields to make adjustments. Use it to add assume_scheme="https"
to URLField
instances like so:
from django import forms
from django.db import models
from example.models import NicheMuseum
def urlfields_assume_https(db_field, **kwargs):
"""
ModelForm.Meta.formfield_callback function to assume HTTPS for scheme-less
domains in URLFields.
"""
if isinstance(db_field, models.URLField):
kwargs["assume_scheme"] = "https"
return db_field.formfield(**kwargs)
class NicheMuseumForm(forms.ModelForm):
class Meta:
model = NicheMuseum
fields = ["name", "website"]
formfield_callback = urlfields_assume_https
For forms that already use formfield_callback
, you’ll need to merge functions as appropriate.
ModelAdmin
form field method override
If you’re using Django’s admin site, that’s another source of outdated URLField
instances. ModelAdmin
classes automatically generate ModelForm
classes, unless a form is explicitly declared. Because this form class is created per-request, the warning won’t appear at import time, but when you load the related “add” or “change” pages:
$ python -Xdev manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
...
/.../django/db/models/fields/__init__.py:1140: RemovedInDjango60Warning: The default scheme will be changed from 'http' to 'https' in Django 6.0. Pass the forms.URLField.assume_scheme argument to silence this warning.
return form_class(**defaults)
[25/Nov/2023 16:37:34] "GET /admin/example/nichemuseum/add/ HTTP/1.1" 200 8907
...
You can also expose the warnings in your test suite by testing all of your ModelAdmin
classes. See my previous post on using parameterized tests to do this easily.
The easiest way to fix these warnings that I know of is with the undocumented ModelAdmin.formfield_for_dbfield()
method. This is passed as formfield_callback
on the generated ModelForm
class and applies some admin-specific customizations. Override it in custom admin classes that you then use as bases in your project:
from django.contrib import admin
from django.contrib.admin.options import BaseModelAdmin
from django.db import models
from example.models import NicheMuseum
# Custom admin classes for project-level modifications
class AdminMixin(BaseModelAdmin):
def formfield_for_dbfield(self, db_field, request, **kwargs):
"""
Assume HTTPS for scheme-less domains pasted into URLFields.
"""
if isinstance(db_field, models.URLField):
kwargs["assume_scheme"] = "https"
return super().formfield_for_dbfield(db_field, request, **kwargs)
class ModelAdmin(AdminMixin, admin.ModelAdmin):
pass
class StackedInline(AdminMixin, admin.StackedInline):
pass
class TabularInline(AdminMixin, admin.TabularInline):
pass
# Admin classes
@admin.register(NicheMuseum)
class NicheMuseumAdmin(ModelAdmin):
...
Third-party packages
URLField
instances in third-party packages may be hard to fix in your project. Some packages do allow you to replace their form classes, so you can use the above techniques. Others don’t, so you may need to resort to monkey-patching.
In either case, projects will hopefully accept pull requests to adopt assume_scheme="https"
, if appropriate. Packages can pass the argument on Django 5.0+ by using this pattern:
from functools import partial
import django
from django import forms
if django.VERSION >= (5, 0):
URLField = partial(forms.URLField, assume_scheme="https")
else:
URLField = forms.URLField
class CheckForm(forms.Form):
url = URLField(label="URL to check")
My tool django-upgrade rewrites such blocks to remove outdated branches. This smooths future maintenance when the package supports Django 5.0 as a minimum.
Fin
Thanks to Coen van der Kamp for opening and working on Ticket #34380 to remove this crusty assumption. And thanks to David Wobrock, Jonathan Sundqvist, and Mariusz Felisiak for reviewing it.
Whilst drafting this post a week before Django 5.0’s final release, I noticed how burdensome adopting all URLField
s would be for most projects. I created a forum post raising this issue. That discussion led to Mariusz Felisiak writing a PR that added the FORMS_URLFIELD_ASSUME_HTTPS
setting. Thanks to all who participated in the discussion and review.
May you be secure by default,
—Adam
Read my book Boost Your Git DX to Git better.
One summary email a week, no spam, I pinky promise.
Tags: django