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 valueclean(self)validates combinations of fields; useself.add_error("field", "msg")or raiseValidationErrorfor a form-wide errorvalidators=[...]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.