Forms & validation

Every real site needs input: sign-ups, enquiries, orders. Django's forms framework renders HTML inputs, converts raw strings into Python types, validates them and reports errors — so you never trust request.POST directly.

A plain Form

# shop/forms.py
from django import forms

class EnquiryForm(forms.Form):
    name = forms.CharField(max_length=80)
    phone = forms.RegexField(regex=r"^[6-9][0-9]{9}$", error_messages={"invalid": "Enter a 10-digit mobile number."})
    city = forms.ChoiceField(choices=[("jaipur", "Jaipur"), ("indore", "Indore"), ("vizag", "Visakhapatnam")])
    quantity_kg = forms.DecimalField(min_value=1, max_digits=6, decimal_places=2)
    message = forms.CharField(widget=forms.Textarea, required=False)

ModelForm: forms built from models

A ModelForm generates fields from a model and can save directly to the database.

from .models import Product

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = ["name", "price", "in_stock"]

List fields explicitly. Using exclude or "__all__" can accidentally expose fields such as is_featured or owner that users should never set.

The view pattern

# shop/views.py
from django.contrib import messages
from django.shortcuts import redirect, render
from .forms import ProductForm

def product_create(request):
    if request.method == "POST":
        form = ProductForm(request.POST, request.FILES)
        if form.is_valid():
            product = form.save()
            messages.success(request, f"Saved {product.name}")
            return redirect("product_list")      # Post/Redirect/Get
    else:
        form = ProductForm()
    return render(request, "shop/product_form.html", {"form": form})

Redirecting after a successful POST stops the browser from resubmitting the form when the user refreshes.

Custom validation

Hook Use it for Raise
clean_<field>() Rules about one field forms.ValidationError
clean() Rules involving several fields ValidationError or self.add_error(field, msg)
Model validators=[...] Rules that must hold everywhere ValidationError
class BookingForm(forms.Form):
    check_in = forms.DateField()
    check_out = forms.DateField()
    gstin = forms.CharField(required=False)

    def clean_gstin(self):
        value = self.cleaned_data["gstin"].strip().upper()
        if value and len(value) != 15:
            raise forms.ValidationError("GSTIN must be 15 characters.")
        return value              # always return the cleaned value

    def clean(self):
        data = super().clean()
        start, end = data.get("check_in"), data.get("check_out")
        if start and end and end <= start:
            self.add_error("check_out", "Check-out must be after check-in.")
        return data

Rendering in the template

<form method="post" enctype="multipart/form-data">
  {% csrf_token %}
  {{ form.non_field_errors }}
  {{ form.as_div }}
  <button type="submit">Save</button>
</form>

form.as_div (Django 5) wraps each field in a <div>. For full control, loop over {% for field in form %} and render field.label_tag, field and field.errors yourself.

Common mistakes

  • Reading request.POST["price"] directly instead of form.cleaned_data["price"].
  • Forgetting request.FILES or enctype="multipart/form-data" when the form has file fields.
  • A clean_<field> method that forgets to return the value, silently turning it into None.
  • Rendering the form again after success instead of redirecting.

Practice

Build a "Bulk order enquiry" page for a sweets shop with name, phone, delivery date and quantity. Reject delivery dates in the past and quantities above 50 kg for same-day delivery, and show a success message after redirecting.

B

Written by Bablu Kumar Jha

Founder, BeeProton AI · Mumbai, Maharashtra, India