Chapter 14 · Forms

14.5 The built-in form fields

The form fields Django ships besides the base class and the choice fields, by the kind of value they make: text, numbers, dates and times, files, booleans, and values made of several fields. Each is drawn by a widget and cleans what was submitted to a value of its own kind.

A built-in field is a subclass of Field that overrides a few of its parts: its widget, messages and validators, widget_attrs for the widget’s HTML attributes, and to_python, which makes the value (A form field: the base class). ChoiceField and its subclasses, FilePathField among them, are told in Choices: ChoiceField, ModelChoiceField and the choice widgets. The examples use a port’s clearance form:

# harbour/fleet/forms.py
class ClearanceForm(forms.Form):
    departs = forms.DateTimeField()
    draught = forms.DecimalField(max_digits=4, decimal_places=2, min_value=0)
    manifest = forms.FileField(required=False)
    hazardous = forms.BooleanField(required=False)

The fields by family

A required field, NullBooleanField aside, refuses an empty value with the code "required"; the last column is what one that is not required makes of "" or None.

Table 14.4 The fields that are not choice fields, the base class aside, by family: text, numbers, dates and times, files, booleans, composites.
field widget cleans to empty
CharField widget TextInput cleans to a str, stripped unless strip=False empty empty_value, "" by default
RegexField widget TextInput cleans to a str, not stripped, in which its pattern is found empty ""
EmailField widget EmailInput cleans to a str, at most 320 characters by default empty ""
URLField widget URLInput cleans to a str with a scheme empty ""
SlugField widget TextInput cleans to a str that is a slug empty ""
UUIDField widget TextInput cleans to a uuid.UUID empty None
GenericIPAddressField widget TextInput cleans to a str, an IPv6 address normalized empty ""
JSONField widget Textarea cleans to the decoded value empty None
IntegerField widget NumberInput cleans to an int empty None
FloatField widget NumberInput cleans to a finite float empty None
DecimalField widget NumberInput cleans to a finite decimal.Decimal empty None
DateField widget DateInput cleans to a datetime.date empty None
TimeField widget TimeInput cleans to a datetime.time empty None
DateTimeField widget DateTimeInput cleans to a datetime.datetime, aware under USE_TZ empty None
DurationField widget TextInput cleans to a datetime.timedelta empty None
FileField widget ClearableFileInput cleans to an uploaded file, the file already there, or False empty None
ImageField widget ClearableFileInput cleans to the same; an upload checked by Pillow empty None
BooleanField widget CheckboxInput cleans to a bool empty False
NullBooleanField widget NullBooleanSelect cleans to True, False or None, even when required empty None
ComboField widget TextInput cleans to what its fields make of it, in turn empty the same
MultiValueField widget TextInput, for a subclass to replace cleans to what compress returns empty what compress returns for []
SplitDateTimeField widget SplitDateTimeWidget cleans to a datetime.datetime, aware under USE_TZ empty None

Numbers

FloatField and DecimalField are subclasses of IntegerField and share its limits: max_value, min_value and step_size become validators, the step counted from the minimum or from 0 (Validators: django/core/validators.py), and IntegerField.widget_attrs writes them on a NumberInput as "min", "max" and "step". For a DecimalField with decimal_places and no step_size, DecimalField.widget_attrs makes the step one unit of the last decimal place, as str writes it, in lower case: "0.01" for the draught, and "1e-7" for seven places, an exponent that, the comment says, keeps a small value from being parsed as 0 (ticket 20765).

IntegerField.to_python strips a trailing point and zeros before calling int, so "3.0" is 3 and "3.5" is refused as "invalid". float and decimal.Decimal accept "inf" and "NaN", and FloatField.validate and DecimalField.validate refuse a value that is not finite.

localize=True makes the widget a TextInput, unless one was given, because, the comment says, a localized number input is not well supported on most browsers; so a localized field given no widget has no "min", "max" or "step". Its value is written in the locale’s format and read with sanitize_separators (Locale formats: get_format and the format modules).

Dates and times

DateField, TimeField and DateTimeField hand a string to BaseTemporalField.to_python, which strips it and tries the field’s input_formats one by one with datetime.datetime.strptime, refusing it as "invalid" when none fits. Formats given as input_formats replace the class’s, which are the locale’s, looked up when the form is cleaned: under "en-us" "10/20/2026" is a date, under "en-gb" it is not (Locale formats: get_format and the format modules).

DateTimeField.to_python first tries parse_datetime, which reads ISO 8601 with any offset; a string of that shape that is no real moment, "2026-02-30 14:30", is refused there. Then come the DATETIME_INPUT_FORMATS and after them the DATE_INPUT_FORMATS, so a date alone is read as its midnight. Under USE_TZ, from_current_timezone makes a naive result aware in the current time zone, and refuses a time that the zone skips or repeats as "ambiguous_timezone"; a result with an offset keeps it. In Malta the clocks went forward at two in the morning of 29 March 2026:

>>> from django.utils import timezone
>>> timezone.activate("Europe/Malta")
>>> form = ClearanceForm({"departs": "2026-10-20 14:30", "draught": "4.2"})
>>> form.is_valid(), form.cleaned_data["departs"]
(True, datetime.datetime(2026, 10, 20, 14, 30, tzinfo=zoneinfo.ZoneInfo(key='Europe/Malta')))
>>> form = ClearanceForm({"departs": "2026-03-29 02:30", "draught": "4.2"})
>>> form.errors.as_data()
{'departs': [ValidationError(['2026-03-29 02:30:00 couldn’t be interpreted in time zone Europe/Malta; it may be ambiguous or it may not exist.'])]}

DateTimeField.prepare_value goes the other way, so that an input shows an aware value in the current zone’s time.

Files

A file input is never drawn with a value, so a form editing a record with a file must tell “no new file” from “remove the file”. Beside a file already there, for a field that is not required, ClearableFileInput draws a box to clear it, and reads the pair as one value: the upload, False for the box alone, or the marker FILE_INPUT_CONTRADICTION for both (Widgets: Widget and its subclasses).

FileField.clean is given the initial value too, by FileField._clean_bound_field. It refuses the marker as "contradiction". It returns False unchecked when the field is not required, and takes it as None when it is. With no upload, an initial value is the cleaned value, unchecked. Anything else goes to Field.clean, where FileField.to_python wants an object with a name and a size, and refuses an empty file unless allow_empty_file=True. When a model form fills its instance, a model’s file field keeps its file for None and is emptied by False (File fields).

FileField.bound_data returns the initial value whatever was sent, so a form drawn again after an error shows the old file, not the new one. FileField.has_changed counts anything but None from the widget as a change, unless the field is disabled.

ImageField.to_python adds Pillow, imported when an image is cleaned. It opens the upload from its path on disk or from its bytes and has Pillow verify the image rather than load it: loading, the comment says, could spot a truncated JPEG but reads the whole image into memory, a denial-of-service vector (tickets 3848 and 18520). Anything Pillow raises becomes "invalid_image". Its validator validate_image_file_extension refuses an extension Pillow does not know, and a file input with no "accept" gets "image/*".

Booleans: required means ticked

An unticked checkbox is absent from the data, and CheckboxInput reads that as False. BooleanField.to_python reads "false", in any case, and "0" as False, and anything else by bool, so "no" is True. BooleanField.validate refuses a false value for a required field: a required BooleanField is a box that must be ticked. A box that may be left empty, like the hazardous box, needs required=False, which a model’s boolean field without choices gives its form field (The field classes).

NullBooleanField reads "True", "true" and "1" as True, their opposites as False and anything else as None, and its validate does nothing, so being required refuses nothing.

Fields made of fields

MultiValueField makes one value of several inputs: it is given a list of fields, and a MultiWidget reads one string for each. MultiValueField.clean refuses as "invalid" a value that is neither empty nor a list or tuple, and, when the field is required, one whose items are all empty. It cleans each item with its field, gathering each distinct error. With require_all_fields, the default, any empty item refuses a required field as "required"; without it, an empty item for a required subfield gives that subfield’s "incomplete" message. The cleaned list goes to compress, which a subclass writes, and the validators run on what it returns; the widget’s decompress is its inverse.

SplitDateTimeField is a DateField and a TimeField drawn by a SplitDateTimeWidget; its compress joins them and passes the result through from_current_timezone. ComboField is the reverse, one value that must pass several fields: after its own required check and validators, its clean hands the value to each field’s clean in turn.

URLs and the assumed scheme

URLField.to_python strips the value and, if it finds no scheme, puts the field’s assume_scheme in front, by default "https": "harbour.example/berths" becomes "https://harbour.example/berths". It takes what precedes the first colon as the scheme when that begins with an ASCII letter and holds no slash; splitting at the colon, the comment says, avoids calling urllib.parse.urlsplit on large or slow-to-normalize inputs.

JSON

JSONField is a CharField drawn as a Textarea, whose to_python decodes with json.loads. A decoded string comes back as a JSONString, and a value that is already a list, a dictionary, a number or a JSONString passes through as it is. JSONField.bound_data wraps text that does not decode in an InvalidJSONInput, which JSONField.prepare_value leaves alone where it writes every other value with json.dumps: text that failed is shown again as it was typed, and text that passed as json.dumps writes it.