Filter concepts by levelShowing all levels.

Django · Forms

Forms

ModelForm, presentation, custom validation, and managing many forms at once

  • ModelForm generates fieldsfrom Meta.model/fields — never "__all__"
  • disabled=True is server-enforcedunlike the HTML readonly attribute
  • clean_<field>() must return a valueclean() runs once, for cross-field rules
  • Formsets need the management formrendered, or submission fails outright

1.ModelForm

ModelForm generates form fields automatically from a model's fields, declared via a Meta class naming the model and which fields to include. save() creates or updates a real model instance directly — save(commit=False) returns the unsaved instance for further changes first.

Key features

  • Meta.model names the model; Meta.fields lists which model fields become form fields — "__all__" works but is a security risk (new model fields are exposed automatically)
  • Field types are inferred from the model: CharField → CharField, ForeignKey → ModelChoiceField, and so on
  • required=True unless the model field has blank=True; label defaults to the model field's verbose_name
  • ArticleForm(instance=article) pre-fills the form for editing; ArticleForm(request.POST, instance=article) binds it for saving the same instance
  • save(commit=False) returns the unsaved instance — needed to set a field (like created_by) before the real save(), or before save_m2m() for a ManyToManyField

ModelForm.Meta options

ModelForm.Meta options
OptionPurpose
modelthe model class to generate fields from
fieldsexplicit whitelist of model fields to include — prefer this over "__all__"
excludeblacklist instead of a whitelist — riskier as the model grows
widgetsoverride the auto-chosen widget for specific fields
labels / help_textsoverride the auto-derived label/help text per field
ModelForm generates fields from the model, then saves back to it

Article (model)

title, body, category

ArticleForm

Meta.model + Meta.fields

is_valid()

save()

creates or updates the instance

  • Article (model)
    • title, body, category
  • ArticleForm
    • Meta.model + Meta.fields
    • is_valid()
  • save()
    • creates or updates the instance

Command

python
class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ["title", "body"]

form = ArticleForm(request.POST, instance=article)  # editing an existing Article
if form.is_valid():
    form.save()

ModelForm generates fields from Meta.model/fields and knows how to save() directly — use commit=False to set a field the form doesn't expose before the real save; always use an explicit fields list, never "__all__".

2.Widgets, labels, initial values, and help text

A widget is the HTML element rendered for a field (Textarea instead of CharField's default text input) — presentation only, separate from validation. label, initial, and help_text all customize the rendered form without touching what values are accepted.

Key features

  • Every field has a default widget (CharField → TextInput) — pass widget=forms.Textarea(...) to override it
  • label overrides the auto-generated field label (default: the field name, prettified)
  • initial sets the value shown on an UNBOUND form only — it has no effect once the form is bound to submitted data
  • help_text renders alongside the field as user-facing guidance — never validated, purely informational
  • A widget can carry HTML attrs (attrs={"rows": 10, "placeholder": "..."}) passed straight through to the rendered tag

Common widget overrides

Common widget overrides
FieldDefault widgetCommon override
CharFieldTextInputTextarea (for a multi-line body)
CharField (password)TextInputPasswordInput (renders type="password")
ChoiceFieldSelectRadioSelect (renders as radio buttons)
BooleanFieldCheckboxInputrarely overridden
DateFieldDateInputa custom widget with type="date" attrs

Command

python
class ContactForm(forms.Form):
    subject = forms.CharField(
        label="Your subject",
        initial="General inquiry",
        help_text="Keep it under 100 characters.",
    )

A widget is presentation only, separate from validation; initial only affects an unbound form's first display and is ignored once the form is bound to submitted data.

3.Custom validation: validators, clean_<field>(), clean()

A validator is a reusable function raising ValidationError for one field; clean_<field>() is a method for one field's own form-specific cleaning, run after the field's own validation, and must return the (possibly transformed) value; clean() runs last, once, for validation spanning multiple fields.

Key features

  • A validator is a plain function/callable, passed via validators=[...] on the field — raises ValidationError, returns nothing
  • clean_<fieldname>() reads self.cleaned_data["fieldname"], and MUST return the value (transformed or not) — the return becomes the new cleaned_data entry
  • clean() runs once, after every field's own clean_<fieldname>(), and must return cleaned_data (or call super().clean() and return that)
  • Raise ValidationError inside clean() for a non-field ("form-wide") error, or call self.add_error("field", msg) to attach it to a specific field instead
  • If a field's own validation fails, its clean_<fieldname>() is skipped — but every OTHER field's validation still runs

Where a validation rule belongs

Where a validation rule belongs
ScopeMechanismWhen it runs
Reusable across formsa validator functionas part of Field.clean()
One field, this form onlyclean_<fieldname>()after that field's own validation
Spans multiple fieldsclean()once, after every field is cleaned
Three validation scopes, narrowest to broadest

a validator

reusable, one field, across forms

clean_<field>()

this form, one field — must return the value

clean()

once, every field together — must return cleaned_data

  • a validator — reusable, one field, across forms
    • leads to clean_<field>()
  • clean_<field>() — this form, one field — must return the value
    • leads to clean()
  • clean() — once, every field together — must return cleaned_data

Command

python
def clean_recipients(self):
    data = self.cleaned_data["recipients"]
    if "fred@example.com" not in data:
        raise ValidationError("You forgot Fred!")
    return data   # required — even if unchanged

clean_<field>() must return the (possibly transformed) value — Django uses whatever it returns; clean() runs once for cross-field rules and must return cleaned_data — use add_error() to attach a message to a specific field from inside it.

4.Errors, disabled/dynamic fields, and file uploads

form.errors is a dict keyed by field name; form.non_field_errors() returns only clean()-level errors with no specific field. disabled=True on a field makes Django ignore any submitted value for it, unlike the HTML readonly attribute. Fields can be added dynamically in __init__(). File uploads need request.FILES alongside request.POST, and enctype="multipart/form-data" in the HTML.

Key features

  • form.errors is a dict: {"field_name": ["error message", ...]} for every field that failed
  • form.non_field_errors() returns only clean()-raised or add_error(None, ...) errors — form.errors also has a "__all__" key holding the same list
  • disabled=True excludes a field from validation entirely and keeps it out of cleaned_data — safer than readonly, which a modified request can still override
  • Fields can be added/removed in __init__(), after calling super().__init__() — self.fields is a plain dict at that point
  • A file-carrying form needs request.FILES as a SECOND constructor argument: MyForm(request.POST, request.FILES) — and the <form> tag needs enctype="multipart/form-data"

disabled vs. HTML readonly

disabled vs. HTML readonly
Aspectdisabled=TrueHTML readonly attr
Enforced byDjango, server-sidethe browser only
Tampered submissionignored — original value keptaccepted as submitted
Appears in cleaned_datanoyes

Command

html
{% if form.is_multipart %}
<form method="post" enctype="multipart/form-data">
{% else %}
<form method="post">
{% endif %}
  {% csrf_token %}
  {{ form }}
</form>

form.errors is keyed by field name; non_field_errors() surfaces only clean()-level errors. disabled=True is enforced server-side (safer than readonly); file uploads need both request.FILES and enctype="multipart/form-data".

5.Formsets and model formsets

A formset is a collection of the same Form rendered and validated together — formset_factory(FormClass) for plain forms, modelformset_factory(Model) for model-backed ones. The management form (hidden fields tracking TOTAL_FORMS etc.) is required in the HTML for the formset to work at all.

Key features

  • formset_factory(MyForm, extra=2) builds a formset class rendering 2 blank forms by default
  • modelformset_factory(Model, fields=[...]) is the model-backed equivalent — formset.save() creates/updates real instances
  • {{ formset.management_form }} MUST be rendered inside the <form> — omitting it breaks the formset entirely, not just cosmetically
  • inlineformset_factory(ParentModel, ChildModel, fields=[...]) is for a model formset scoped to one parent's related objects (e.g. all of one Author's Books)
  • max_num/extra control how many forms render; edit_only=True on a model formset prevents creating new objects through it

The three formset factories

The three formset factories
FactoryForsave() does
formset_factory(Form)plain, non-model formsnothing — plain forms have no save()
modelformset_factory(Model)many instances of one modelcreates/updates instances
inlineformset_factory(Parent, Child)a parent's related objectscreates/updates, scoped to the parent

Command

html
<form method="post">
  {% csrf_token %}
  {{ formset.management_form }}
  {% for form in formset %}{{ form }}{% endfor %}
  <button type="submit">Save all</button>
</form>

formset_factory is for plain forms, modelformset_factory/inlineformset_factory for model-backed ones — {{ formset.management_form }} must always be rendered, or the whole formset fails on submission.

Remember

  • ModelForm generates fields from Meta.model/fields — use commit=False to set a field the form doesn't expose before the real save.
  • A widget is presentation only; initial only affects an unbound form's first display, never a bound one.
  • clean_<field>() must return the cleaned value; clean() runs once for cross-field rules via add_error().
  • A file-carrying form needs both request.FILES and enctype="multipart/form-data".

Pro tip

  • Always use an explicit fields list on a ModelForm, never "__all__" — a new model field shouldn't become editable by accident.
  • inlineformset_factory automatically scopes every form to one parent's related objects — no manual filtering needed.

Next up

  • Django Models
  • Django Field Types
  • null vs blank

Sources

  • ModelForm — docs.djangoproject.com/en/5.2/topics/forms/modelforms/
  • Form and field validation — docs.djangoproject.com/en/5.2/ref/forms/validation/
  • Every claim checked against Django 5.2 LTS docs · checked 2026-08-21

Spotted a mistake on this page? Tell us.