Django

Django Forms & Validation👨‍💻

Django's forms framework handles the entire round-trip of user input: rendering HTML fields, parsing submitted data, running validation, and reporting errors. At its core, a form is a collection of fields that each know how to coerce a raw string into a Python object and reject values that don't make sense. The framework gives you three layers of validation hooks — field-level, per-field custom, and cross-field — so you can express constraints exactly where they belong.

What makes Django forms worth learning deeply is the lifecycle. Every call to is_valid() triggers full_clean(), which orchestrates _clean_fields(), _clean_form(), and _post_clean() in a specific order. Understanding this pipeline is the difference between forms that 'just work' and forms where errors disappear silently or validation runs against stale data. Once you internalize the lifecycle, ModelForm, formsets, and inline formsets are just variations on the same pattern.

Key Takeaways

  • 1A form is **bound** when instantiated with `data=` — only bound forms can be validated. Passing `initial=` does NOT bind the form.
  • 2`is_valid()` triggers `full_clean()`, which runs `_clean_fields()` (per-field validation), then `_clean_form()` (cross-field via `clean()`), then `_post_clean()` (ModelForm-specific).
  • 3Each `clean_<fieldname>()` method receives the already-validated value from `self.cleaned_data` and must return the cleaned value — forgetting the return sets the field to `None`.
  • 4The `clean()` method handles cross-field validation. Always use `.get()` to access `cleaned_data` here because fields that failed earlier are absent from the dictionary.
  • 5ModelForm bridges forms and ORM models — it generates fields from model definitions, runs model-level validation in `_post_clean()`, and provides `save()` for persistence.
  • 6Formsets manage collections of identical forms with built-in support for creation, editing, deletion, and ordering of multiple objects.

Master django forms & validation

Take the Django Forms & Validation course with hands-on lessons and challenges.

Examples

Form lifecycle — bound vs unbound, validation, cleaned_data

python

A form becomes bound when you pass data= to its constructor. Only bound forms can be validated. The cleaned_data dictionary is populated by is_valid(), which internally calls full_clean(). Accessing cleaned_data before calling is_valid() raises an AttributeError.

clean_<field>() and clean() — per-field and cross-field validation

python

clean_<fieldname>() runs per-field custom validation after the built-in field.clean() has already type-checked and coerced the value. The clean() method runs last and is the place for validation that depends on multiple fields. Always call super().clean() first and use .get() because earlier field failures remove keys from cleaned_data.

Cross-field validation with add_error() — associating errors with specific fields

python

add_error() lets you attach a cross-field validation error to a specific field instead of it appearing as a non-field error. This gives users clearer feedback because the error renders next to the relevant input. Note that add_error() also removes the field from cleaned_data, so subsequent code must handle the missing key.

ModelForm — automatic fields from a model with save()

python

ModelForm reads the model's field definitions and generates matching form fields automatically. The Meta class controls which fields to include and how to render them. Passing instance= to the constructor pre-fills the form for editing. The save() method creates or updates the model instance depending on whether instance has a primary key.

Formsets — managing multiple forms as a collection

python

formset_factory() creates a formset class that manages multiple instances of the same form. The clean() method on a custom BaseFormSet subclass handles validation across all forms in the set — for example ensuring no duplicate products. The management form (rendered with {{ formset.management_form }}) tracks how many forms exist in the HTML.

Common Mistakes

Mistake:

Forgetting to return the cleaned value from `clean_<fieldname>()` — the field silently becomes `None`

Fix:

Every `clean_<fieldname>()` method must explicitly return the value. If you only validate without returning, the field's `cleaned_data` entry becomes `None`, which causes subtle bugs downstream. Always end with `return value`.

Mistake:

Accessing `cleaned_data['field']` directly inside `clean()` instead of using `.get()` — raises `KeyError` when a field fails validation

Fix:

In the `clean()` method, fields that failed their own validation are removed from `cleaned_data`. Use `cleaned_data.get('field')` and guard with `if field_a and field_b:` before comparing values.

Mistake:

Confusing `initial=` with `data=` — passing `initial={'name': 'Default'}` and expecting the form to be bound

Fix:

`initial=` sets default display values for unbound forms. Only `data=` makes a form bound and triggers validation when `is_valid()` is called. An unbound form's `is_valid()` always returns `False`.

Mistake:

Using `fields = '__all__'` in ModelForm Meta — exposes every model field including sensitive ones

Fix:

Always list fields explicitly: `fields = ['title', 'content', 'status']`. Using `__all__` or not specifying `fields` can expose internal fields (like `is_staff` on a User model) to form input, creating security vulnerabilities.

Best Practices

  • Keep single-field validation in `clean_<fieldname>()` and cross-field validation in `clean()` — this ensures errors are attached to the correct field in the rendered form
  • Always call `super().clean()` at the start of your `clean()` override to preserve parent validation logic and ensure `cleaned_data` is properly populated
  • Use `add_error('fieldname', message)` instead of raising `ValidationError` in `clean()` when the error relates to a specific field — this gives users inline error placement
  • Prefer explicit `fields = [...]` over `exclude = [...]` in ModelForm Meta — exclude is fragile because adding a new model field automatically exposes it in the form
  • Write reusable validators as standalone functions or classes using `django.core.validators` and attach them via the `validators=[]` field parameter instead of duplicating logic across clean methods
  • For formsets, always validate the management form data — a tampered `TOTAL_FORMS` value can cause unexpected behavior. Set `max_num` and `min_num` to enforce bounds

Summary

Django forms handle the full input lifecycle: rendering, parsing, validation, and error reporting. The validation pipeline runs in a strict order — field-level `clean()`, then per-field `clean_<fieldname>()`, then cross-field `clean()`, and finally `_post_clean()` for ModelForm. Understanding this order is essential for placing validation logic correctly. ModelForm bridges forms and the ORM by generating fields from model definitions and providing `save()` for persistence. Formsets extend the pattern to collections of forms with cross-form validation. The key habits: always return from `clean_<fieldname>()`, use `.get()` in `clean()`, list fields explicitly in ModelForm, and use `add_error()` when a cross-field error belongs to a specific field.

Practice Django with hands-on challenges

Learn django forms & validation hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Django with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.