skip to content

Fields & Widgets

A Form class declares Field objects that coerce input and Widget objects that render HTML, and passing data= makes it bound. Interviewers probe bound vs unbound, data vs initial and cleaned_data.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

6

In Django forms, what is the difference between a bound and an unbound form, and when does cleaned_data exist?

level: juniorimportance: must knowfreq 72%

answer

  1. what the constructor received
  2. the is_bound attribute
  3. initial is display-only
  4. is_valid() triggers full_clean()
  5. empty POST and the or-None idiom

basics

~20 s

A Django form is bound when it is built with data= or files= (even an empty dict) and unbound otherwise. Only a bound form validates; cleaned_data appears once is_valid() or errors runs full_clean() on it.

solid answer

~40 s

Django sets `form.is_bound` to True when `data` or `files` is not `None`, so `SignupForm(request.POST)` is bound while `SignupForm()` and `SignupForm(initial={...})` are unbound. An unbound form only renders, pre-filled from `initial`; `is_valid()` on it returns False without validating. On a bound form, the first call to `is_valid()` or `form.errors` runs `full_clean()`, which builds `cleaned_data`: each field's input coerced to a Python value such as a `date`, a `bool` or a stripped string. A field that fails validation is left out of `cleaned_data` and reported in `errors`, so you read `cleaned_data` only after `is_valid()` returns True. For ordinary enabled fields `initial` is not a fallback: a field missing from the submission is treated as empty.

code

python · 15 lines
python
from django import forms

class NewsletterSignupForm(forms.Form):
    email = forms.EmailField()
    first_name = forms.CharField(required=False, max_length=50)

unbound = NewsletterSignupForm(initial={'email': '[email protected]'})
unbound.is_bound      # False
unbound.is_valid()    # False, nothing was validated
# unbound.cleaned_data -> AttributeError

bound = NewsletterSignupForm(data={'email': ' [email protected] '})
bound.is_bound        # True
bound.is_valid()      # True
bound.cleaned_data    # {'email': '[email protected]', 'first_name': ''}

go deeper

for a junior

Recall the rule: data= or files= binds the form, initial= does not. Say that cleaned_data exists only after is_valid() or errors has run.

for a middle

Explain full_clean(): the widget pulls raw input, the field coerces it, failing fields drop out of cleaned_data into errors, and unbound forms stop before any of it.

for a senior

Show you know the traps in real views: request.POST or None on an empty POST, re-reading request.POST instead of cleaned_data, and assuming initial fills gaps.

for a principal

Frame binding as the boundary between untrusted input and trusted values, and argue for team conventions that only ever persist cleaned_data.

## What binding means A Django `Form` instance is in one of two states, decided entirely by its constructor arguments: - **Bound**: `data` or `files` is not `None`. Django sets `form.is_bound = True` and treats `data` as the user's submission. - **Unbound**: both are `None`. The form can only be rendered, empty or pre-filled from `initial`. The check is a literal `is not None` test. `SignupForm(data={})` is therefore **bound** (an empty submission that will fail every required field), while `SignupForm(initial={...})` is **unbound** however much initial data it carries. | Call | `is_bound` | What `is_valid()` does | |---|---|---| | `SignupForm()` | False | returns False, runs nothing | | `SignupForm(initial={'email': '[email protected]'})` | False | returns False, runs nothing | | `SignupForm(data={})` | True | validates; required fields fail | | `SignupForm(request.POST, request.FILES)` | True | validates the submission | ## When cleaned_data appears `cleaned_data` is not an attribute of a freshly built form. It is created by `full_clean()`, which Django runs lazily the first time you call `is_valid()` or read `form.errors`: 1. `full_clean()` creates an empty error dictionary. For an unbound form it stops there, so an unbound form never gains `cleaned_data` and reading it raises `AttributeError`. 2. For a bound form it creates `cleaned_data = {}` and cleans each field: the widget extracts the raw value from `data` or `files`, the field coerces it into a Python object, and the result is stored under the field's name. 3. Form-wide cleaning and an internal hook that `ModelForm` uses run afterwards (their order is the validation topic's material). What ends up in `cleaned_data` is **typed, normalised data**: a `datetime.date` for a `DateField`, a `bool` for a `BooleanField`, a whitespace-stripped string for a `CharField` (`strip=True` is the default). A field that raised a `ValidationError` is **not** in `cleaned_data`; its message sits in `form.errors`. Hence the working rule: read `cleaned_data` only after `is_valid()` returned True, and never go back to `request.POST` for a value the form has already cleaned. ## data versus initial The two dictionaries look alike and do different jobs: - **`data`** is what the user submitted. It is validated and becomes `cleaned_data`. - **`initial`** is what an unbound form displays. It is never validated. You can pass it to the form or declare it on the field; the form-level dictionary wins, and a callable such as `datetime.date.today` is called each time it is needed. - In a bound form, an ordinary enabled field that is missing from `data` is treated as **empty**, not filled from `initial`. A `CharField(required=False, initial='weekly')` absent from the submission cleans to `''`. - `initial` still matters to a bound form in narrow places: `has_changed()` and `changed_data` compare the submission against it, a `disabled=True` field cleans its initial value instead of submitted data, and a `FileField` with no new upload keeps its initial file. ## The request.POST or None idiom Many function views write `form = SignupForm(request.POST or None)` to share one code path for GET and POST. It works because an empty `QueryDict` is falsy, so a GET builds an unbound form. The edge is a POST whose body carries no fields at all: it also builds an unbound form, and the view silently re-renders without errors instead of reporting the required fields. In a normal template form the CSRF token keeps the POST non-empty, but branching on `request.method == 'POST'` states the intent without relying on truthiness. ## A newsletter sign-up example ```python from django import forms class NewsletterSignupForm(forms.Form): email = forms.EmailField() first_name = forms.CharField(required=False, max_length=50) f = NewsletterSignupForm(data={'email': ' [email protected] '}) f.is_valid() # True f.cleaned_data # {'email': '[email protected]', 'first_name': ''} ``` The address arrives padded with spaces and leaves stripped; the missing optional name cleans to the `CharField`'s empty value, the empty string. ## What interviewers listen for - Binding is decided by `data` and `files`, not by the HTTP method and not by `initial`. - `is_valid()` on an unbound form returns False without raising. - `cleaned_data` holds coerced Python values, only for the fields that passed. - `initial` is display data, not a default for missing input, apart from disabled fields and kept files.

  • In a Django view, why can SignupForm(request.POST or None) misbehave on a POST request?
    An empty `QueryDict` is falsy, so a POST that carries no fields produces an unbound form. `is_valid()` then returns False without errors and the view re-renders silently. The CSRF token usually keeps template POSTs non-empty, but checking `request.method == 'POST'` avoids the ambiguity entirely.
  • In Django, does a bound form that failed validation still have a cleaned_data attribute?
    Yes. After `full_clean()` runs, `cleaned_data` exists on any bound form, but it holds only the fields that passed; each failing field was removed and its message placed in `form.errors`. It is useful for diagnostics, not for saving.
  • When both a Django Form's initial= dict and a field's initial= give a value, which one is displayed?
    The form-level `initial` dictionary wins; the field's `initial` is only used when the form's dictionary has no entry for that name. Either may be a callable, which Django calls when the value is needed, so `datetime.date.today` stays current.

saying these in an interview costs you the question

  • Passing initial= to a Django form makes it bound and validates it.
  • An empty dict passed as data leaves the form unbound.
  • cleaned_data holds the raw strings exactly as they arrived in request.POST.
  • A field missing from submitted data falls back to its initial value.
  • Calling is_valid() on an unbound form raises an exception.
open as a page

In Django forms, how do Field and Widget responsibilities differ, and how would you give a sign-up form's date field a native date picker?

level: middleimportance: must knowfreq 56%

basics

~20 s

A Django Field coerces and validates input into a Python value; its Widget renders the HTML and extracts the raw value. For a native picker use DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), because browsers accept only ISO dates.

open as a page

In Django forms, what does a field's required flag actually check, and why does a BooleanField reject an unticked checkbox?

level: juniorimportance: should knowfreq 46%

basics

~20 s

A Django field with required=True (the default) rejects a cleaned value that is empty: None, '', or an empty list, tuple or dict. An unticked checkbox cleans to False, and a required BooleanField treats False as missing.

open as a page

Why does a Django form's FileField report 'This field is required.' even though the user chose a file to upload?

level: middleimportance: should knowfreq 50%

basics

~20 s

A Django FileField's widget reads only from the form's files dict, so the form must be built with files=request.FILES, and the HTML form must use enctype="multipart/form-data"; otherwise request.FILES is empty and the field sees no file.

open as a page

In a Django form, why is a field with disabled=True safer than a readonly widget attribute for a value subscribers must not change?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A Django field with disabled=True ignores whatever is submitted and cleans the form's initial value, so a tampered POST cannot change it. A readonly attribute is only a browser hint; the submitted value is validated and used.

open as a page

In Django forms, how do you set a ChoiceField's choices per request, and why does cleaned_data return strings rather than integers?

level: middleimportance: nice to knowfreq 32%

basics

~10 s

Set Django ChoiceField choices per request by passing a callable or assigning self.fields['name'].choices in the form's init, never on the class. ChoiceField cleans to strings; use TypedChoiceField with coerce=int for integers.

open as a page