Forms and validation

Django · Practical · 14 min

Forms are where untrusted input enters your app; Django validates, converts and re-displays it for you.

A form class declares fields, each with a type, widget and validation:

class ContactForm(forms.Form):
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea, max_length=2000)

The standard view pattern:

1. GET: create an unbound form, ContactForm(), and render it. 2. POST: create a bound form, ContactForm(request.POST), and call form.is_valid(). 3. If valid, use form.cleaned_data, which holds converted Python values (int, date, ...), then redirect. 4. If invalid, re-render the same bound form: it remembers the input and carries form.errors.

Custom validation:

  • clean_<field>(self) validates one field and returns the cleaned value
  • clean(self) validates combinations of fields; use self.add_error("field", "msg") or raise ValidationError for a form-wide error
  • validators=[...] on a field for reusable checks

In templates, {{ form }} renders all fields with labels and errors (as <div>s by default). For custom layouts, loop over fields or use {{ form.email.as_field_group }} per field, and always include {% csrf_token %} in POST forms, or the CSRF middleware rejects the submission with 403.

Never read request.POST directly for business logic when a form exists: cleaned_data is the validated, typed version. File uploads need request.FILES passed as the second argument and enctype="multipart/form-data" on the form.

Example

from django import forms
from django.shortcuts import redirect, render

class ContactForm(forms.Form):
    email = forms.EmailField()
    subject = forms.CharField(max_length=100)
    message = forms.CharField(widget=forms.Textarea, min_length=10)
    cc_myself = forms.BooleanField(required=False)

    def clean_subject(self):
        return self.cleaned_data["subject"].strip().capitalize()

    def clean(self):
        cleaned = super().clean()
        if "http" in cleaned.get("message", "") and not cleaned.get("cc_myself"):
            self.add_error("message", "Links are only allowed if you cc yourself.")
        return cleaned

def contact(request):
    form = ContactForm(request.POST or None)          # bound on POST, unbound on GET
    if request.method == "POST" and form.is_valid():
        send_message(**form.cleaned_data)              # typed, validated values
        return redirect("contact-thanks")
    return render(request, "contact.html", {"form": form})
# template: <form method="post">{% csrf_token %}{{ form }}<button>Send</button></form>

Remember

  • Unbound form for GET, bound form(request.POST) for POST
  • is_valid() fills cleaned_data or errors
  • clean_<field>() for one field, clean() for cross-field rules
  • Always include {% csrf_token %} in POST forms

Watch out for

  • Rendering a fresh empty form after a failed POST, losing the user's input and the error messages.

Check yourself

  1. After form.is_valid() returns True, where should you read the submitted values from?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.djangoproject.com/en/6.0/topics/forms/