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 classHoldsRenders as
CharFieldText<input type="text">
EmailFieldText, validated as an email address<input type="email">
IntegerFieldA whole number<input type="number">
ChoiceFieldOne value from a fixed list of choices<select>
BooleanFieldTrue / 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.

Next up: Writing a plain Form for a model like Task means retyping every field Django already knows about. ModelForms generate that form for you, straight from the model.