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 ofform.cleaned_data["price"]. - Forgetting
request.FILESorenctype="multipart/form-data"when the form has file fields. - A
clean_<field>method that forgets toreturnthe value, silently turning it intoNone. - 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.