Django Forms
Every framework eventually needs a way to collect input from a visitor and check it before
trusting it. You could do this the hand-rolled way: write raw <input> tags in
HTML yourself, then in your view manually pull values out of request.POST, check
each one for emptiness or the right shape, and build up an errors dictionary by hand. It works,
but it's tedious, easy to get subtly wrong, and every field adds another block of repeated
checking code. Django's Form class replaces all of that with a single Python class:
define your fields once, and Django gives you validation, error messages, and HTML rendering for
free, all driven from that one definition.
Defining a Form Class
A form is a plain Python class that inherits from forms.Form, with one class
attribute per field you want to collect. Here's a small search form for TaskFlow, letting a
visitor search tasks by keyword and optionally narrow the results to one priority level:
# taskflow/forms.py
from django import forms
class TaskSearchForm(forms.Form):
PRIORITY_CHOICES = [
('', 'Any priority'),
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
]
query = forms.CharField(required=False, label='Search')
priority = forms.ChoiceField(choices=PRIORITY_CHOICES, required=False)
Nothing here touches HTML or request.POST directly. query is declared
as a CharField, priority as a ChoiceField restricted to
four exact values, and both are marked required=False since an empty search should
simply return every task rather than raise a validation error.
Field Types at a Glance
Django ships a field class for every common kind of input. A few of the most-used ones:
| Field class | Holds | Renders as |
|---|---|---|
CharField | Text | <input type="text"> |
EmailField | Text, validated as an email address | <input type="email"> |
IntegerField | A whole number | <input type="number"> |
ChoiceField | One value from a fixed list of choices | <select> |
BooleanField | True / False | <input type="checkbox"> |
Each field class also carries its own validation rules automatically — an EmailField
rejects anything that isn't a well-formed email address, an IntegerField rejects
text that isn't a whole number, all before your view code ever sees the value.
Rendering a Form in a Template
Once a view hands a form instance to a template as context, rendering it can be as short as one
variable. {{ form }} outputs each field's label and widget, one after another, with
no wrapping markup at all:
{{ form }}
{{ form.as_p }} is usually a better starting point — it wraps each field (label,
widget, and any error messages) inside its own <p> tag, which is enough
structure to look reasonable without writing any HTML yourself:
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Search</button>
</form>
Both of these are convenient, but notice neither gives you control over the exact markup around
each field — you get whatever wrapping Django chooses. When a design calls for specific classes
or layout around individual fields, the alternative is rendering each field by hand with
{{ form.query }} and {{ form.priority }} separately, which we'll put to
full use once TaskFlow's templates get more design attention.
Handling Form Submission in a View
A view that uses a form typically handles two situations: a GET request, where it should just display an empty (or previously-filled) form, and a POST request, where it should validate the submitted data and act on it. The standard pattern instantiates the form differently depending on which one it is:
# taskflow/views.py
from django.shortcuts import render
from .forms import TaskSearchForm
from .models import Task
def task_search(request):
results = None
if request.method == 'POST':
form = TaskSearchForm(request.POST)
if form.is_valid():
query = form.cleaned_data['query']
priority = form.cleaned_data['priority']
results = Task.objects.all()
if query:
results = results.filter(title__icontains=query)
if priority:
results = results.filter(priority=priority)
else:
form = TaskSearchForm()
return render(request, 'taskflow/task_search.html', {'form': form, 'results': results})
On GET, TaskSearchForm() is created with no data at all — an empty form, ready to be
filled in. On POST, TaskSearchForm(request.POST) is created bound to the
submitted data. Calling form.is_valid() runs every field's validation, and only once
it returns True is form.cleaned_data available — a dictionary of the
submitted values, already converted to the right Python types and safe to use.
Validation and the clean_<field>() Method
Beyond the automatic checks each field class already performs, a form can define its own
per-field validation by adding a method named clean_ followed by the field's name.
Django calls that method automatically during is_valid(), after the field's own
built-in validation has already passed. Here's TaskSearchForm extended to reject a
search query that's nothing but spaces — technically non-empty, but not a real search term:
# taskflow/forms.py
class TaskSearchForm(forms.Form):
# ... query and priority fields as above ...
def clean_query(self):
query = self.cleaned_data['query']
if query and not query.strip():
raise forms.ValidationError("Search can't be just whitespace.")
return query.strip()
Two things to notice: the method reads the field's already-cleaned value out of
self.cleaned_data (not request.POST directly), and it must
return the value it wants stored as the final cleaned result — here, the
query with surrounding whitespace stripped off. If the value fails the check, raising
forms.ValidationError attaches an error message to that specific field, and
form.is_valid() returns False for the whole form.
Form for a model like Task
means retyping every field Django already knows about. ModelForms
generate that form for you, straight from the model.