23.8 What changed in forms and validators
The changes from Django 5.0 to this commit in django.forms and django/: the div layout as the default, RenderableFieldMixin, bound_field_class, the ARIA attributes, the URL field’s "https", the blank choice’s label, and EmailValidator’s new methods.
Most of the change is in the HTML a form draws; the fields and validators changed a default or a hook at a time.
The layout: divisions, and a template for each field
Since 5.0 a form draws itself as divisions: BaseRenderer is "django/ and BaseRenderer "django/, and str(form) renders the renderer’s. The default had been the table layout, through the templates django/forms/default.html and django/forms/formsets/default.html, which 5.0 removed. The transitional renderers DjangoDivFormRenderer and Jinja2DivFormRenderer, which drew divisions early, were deprecated in 5.0 and removed in 6.0. The other layouts remain, as the as_table, as_p and as_ul of RenderableFormMixin; the method _html_output of BaseForm, which drew them in Python before 4.0, went in 5.0 (Rendering a form: renderers and templates).
The division template hands each field to a template of its own (5.0). It prints {{ field, and RenderableFieldMixin renders the bound field’s template, which draws label, help text, errors and widget. That template is one given to the form field as template_name=, or else the renderer’s BaseRenderer, "django/. The paragraph, table and list templates still draw each field’s parts themselves.
Rendering an error list or dictionary to a plain string, not a safe one, was deprecated in 4.0 and stopped working in 5.0. BaseForm hands the template each field’s ErrorList as it is, and the template escapes a plain string: in a run, an error class whose __str__ returned "<b>…</b>" unmarked drew <b>. Django’s own error lists are marked safe by RenderableMixin.
bound_field_class
Before 5.2 a field had bound fields of another class only by an override of Field. Since 5.2 that method takes the class from the field’s Field, else from the form’s, else BoundField. The form’s is settled by BaseForm: its argument, else its class’s BaseForm, else the renderer’s BaseRenderer, None on Django’s renderers. So the project’s renderer can give every form one class of bound field (BoundField: a field in a form).
The attributes for screen readers
From 5.0 to 5.2 a field was tied to its help text and errors, for screen readers. 5.0 gave the input "aria-describedby", naming the help text, and "aria-invalid" set to "true" when the field has errors; 5.1 put "aria-describedby" on the <fieldset> the field’s template draws for a widget whose Widget is true, such as a RadioSelect; 5.2 added the errors to it. At this commit BoundField (5.2) joins BoundField with "_helptext" and "_error", as the field has help text and errors. BoundField puts it on the widget unless the widget has its own or asks for a fieldset; then "django/ puts it on the fieldset. The errors’ id is written by "django/ from ErrorList (5.2), which BaseForm sets to the field’s auto_id.
# BerthForm of harbour/fleet/forms.py, tonnage with help_text="Gross tons.", in a shell
>>> form = BerthForm({"ship": "Heron", "tonnage": "0", "arrives": "2026-10-20"})
>>> print(form["tonnage"].as_field_group())
<label for="id_tonnage">Tonnage:</label>
<div class="helptext" id="id_tonnage_helptext">Gross tons.</div>
<ul class="errorlist" id="id_tonnage_error"><li>Ensure this value is greater than or equal to 1.</li></ul>
<input type="number" name="tonnage" value="0" min="1" required aria-invalid="true" aria-describedby="id_tonnage_helptext id_tonnage_error" id="id_tonnage">
The URL field’s scheme
In 5.0 the scheme URLField puts in front of a value that has none became URLField, an argument whose default, "http", was deprecated at once; in 6.0 the default became "https", and FORMS_URLFIELD_ASSUME_HTTPS, the transitional setting that took it early, was removed. Nothing in Django passes the argument, so the form field a model’s URL field makes assumes "https" too (The built-in form fields).
The blank choice’s label
A blank choice, the empty option that a model field’s choices and a ModelChoiceField’s begin with, was labelled with dashes. Since 6.1 its label comes from get_blank_choice_label: BLANK_CHOICE_LABEL, a translated "- Select an option -", or the dashes of BLANK_CHOICE_DASH while USE_BLANK_CHOICE_DASH is true. It reads the constant at each call, so an application may replace it; the model side is in What changed in models and fields. The setting and the constant are deprecated, to go in Django 2028 (What still warns: the deprecations and the releases that end them). Defining the setting warns with RemovedInDjango2028Warning; in a run, reading the constant did not (Choices: ChoiceField, ModelChoiceField and the choice widgets).
EmailValidator
Before 6.2 a subclass could replace the domain’s check in EmailValidator through EmailValidator, which answers true or false. Now the call is EmailValidator, then EmailValidator, which hands the user part to EmailValidator and the domain to EmailValidator; each raises ValidationError itself, with the part as the parameter "username" or "domain" beside "value". The old method is deprecated, to go in Django 2029: a subclass that defines it warns with RemovedInDjango2029Warning as the class is made, and validate then calls it in place of validate_domain (EmailValidator; Validators: django/).
The smaller changes
| name | what changed | release | where now |
|---|---|---|---|
ColorInput, SearchInput, TelInput |
what changed new inputs of the types "color", "search" and "tel"; no Django field uses them |
release 5.2 | where now ColorInput django/ |
Script |
what changed a script of a Media declaration, with HTML attributes |
release 5.2 | where now Script django/ |
Stylesheet |
what changed the same for a stylesheet; at this commit a path given as a string becomes one, or a Script |
release 6.1 | where now Media django/Media django/Media |
File |
what changed reads the directory again; the field calls it when made, and a form’s __init__ may call it |
release 6.1 | where now File django/ |
Base |
what changed its second argument, once named instance, is named obj; Django passes it by position | release 5.0 | where now Base django/ |
Step |
what changed takes an offset to count the steps from; at this commit the form IntegerField and its subclasses give min_value= as the offset |
release 5.0 | where now Step django/Integer django/ |
Domain |
what changed new; at this commit URLValidator and EmailValidator take its host-name patterns; a newline refused since a security fix (Security fixes: disclosure, spoofing and privilege) |
release 5.1 | where now Domain django/ |