Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Django validates a submitted form when you call form.is_valid() (or access form.errors). That call runs field cleaning, converts accepted values to Python types, applies validators, executes cross-field checks in clean(), and— for a ModelForm—runs the relevant model validation. Read normalized values from cleaned_data only after validation succeeds.

The complete Django validation workflow

A form must be bound to input before it can validate. In a view, pass request.POST and, for uploads, request.FILES to the form. An unbound form such as ProfileForm() only renders fields; it has no submitted values to check.

  1. Construct the form with submitted data: form = SignupForm(request.POST).
  2. Call form.is_valid(). This runs the cleaning pipeline and returns a Boolean.
  3. If it returns True, use normalized values in form.cleaned_data.
  4. If it returns False, render the bound form again. Django exposes field errors through form.errors and non-field errors through form.non_field_errors().

Accessing form.errors also triggers validation. Do not read cleaned_data as if every key were present when the form is invalid: fields that fail cleaning are omitted, while valid fields remain available.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A minimal view

from django.shortcuts import render, redirect
from .forms import ContactForm

def contact(request):
    if request.method == "POST":
        form = ContactForm(request.POST, request.FILES)
        if form.is_valid():
            # cleaned_data contains Python values, not raw strings
            send_message(form.cleaned_data)
            return redirect("contact_done")
    else:
        form = ContactForm()
    return render(request, "contact.html", {"form": form})

For a file field, omitting request.FILES means the upload cannot be validated or saved correctly. Keep the request method check so a GET displays an unbound form and a POST preserves entered values and errors.

Field cleaning: required checks, conversion and validators

Every Django Field has a clean(value) method. It either returns a cleaned value or raises django.core.exceptions.ValidationError. Required fields reject None and empty input by default. Set required=False when empty input is valid.

Cleaning also normalizes values. For example, a valid DateField becomes a Python datetime.date, and an integer field becomes an integer. This is why business logic should use cleaned_data instead of parsing request.POST manually.

Reusable validators

Use a validator when the same rule can be attached to several fields or reused outside one form.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from django import forms
from django.core.validators import RegexValidator

username_validator = RegexValidator(
    regex=r"^[a-z0-9_]+$",
    message="Use lowercase letters, numbers and underscores only.",
)

class ProfileForm(forms.Form):
    username = forms.CharField(
        max_length=30,
        validators=[username_validator],
    )
    birth_date = forms.DateField(required=False)

Validators should raise ValidationError, not return an error string. Keep field-specific rules close to the field declaration when possible.

The clean_<fieldname>() hook

Override clean_email(), clean_amount(), or another field hook when validation belongs to one field but needs form state or custom normalization.

from django import forms
from django.core.exceptions import ValidationError

class AccountForm(forms.Form):
    email = forms.EmailField()

    def clean_email(self):
        email = self.cleaned_data["email"].strip().lower()
        if email.endswith("@example.invalid"):
            raise ValidationError("That email domain is not accepted.")
        return email

Return the value you want stored in cleaned_data. If you raise an error, Django attaches it to that field.

Cross-field rules with clean()

Override the form’s clean() method for relationships involving multiple fields: matching passwords, date ranges, or fields that become mandatory only under a particular status. Field cleaning has already run, so inspect self.cleaned_data and, when useful, self.errors.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from django import forms
from django.core.exceptions import ValidationError

class RegistrationForm(forms.Form):
    password = forms.CharField(widget=forms.PasswordInput)
    password_again = forms.CharField(widget=forms.PasswordInput)
    start_date = forms.DateField()
    end_date = forms.DateField(required=False)

    def clean(self):
        cleaned = super().clean()
        password = cleaned.get("password")
        password_again = cleaned.get("password_again")
        start = cleaned.get("start_date")
        end = cleaned.get("end_date")

        if password and password_again and password != password_again:
            self.add_error("password_again", "Passwords do not match.")
        if start and end and end < start:
            raise ValidationError("End date must be on or after start date.")
        return cleaned

An error raised directly from clean() is a non-field error because it concerns the form as a whole. Use self.add_error("field", message) when the message should appear beside a particular field. Always return the cleaned dictionary returned by super().clean() (or your modified version).

ModelForm validation and uniqueness

A ModelForm combines form and model validation. Calling is_valid(), reading errors, or calling full_clean() starts the process. Django first runs the form’s field cleaning and clean(), then validates the model instance for fields represented on the form.

from django import forms
from .models import Article

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ["title", "body", "publish_at"]

    def clean(self):
        cleaned = super().clean()
        publish_at = cleaned.get("publish_at")
        body = cleaned.get("body")
        if publish_at and not body:
            self.add_error("body", "A scheduled article needs body text.")
        return cleaned

Include only fields users are allowed to edit. Omitted model fields are excluded from the ModelForm’s user-facing validation so a user can correct errors on fields present in the form.

Call super().clean() when overriding ModelForm.clean() if you want Django’s uniqueness checks for unique, unique_together, and unique_for_date, unique_for_month, or unique_for_year to remain enabled. Skipping the parent implementation can remove those checks.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Model validation, full_clean(), and save()

Model.full_clean() performs four stages in order:

  1. clean_fields() validates individual model fields.
  2. clean() applies model-wide rules.
  3. validate_unique() checks uniqueness.
  4. validate_constraints() checks model constraints.

save() does not call full_clean() automatically. If application code creates a model instance directly and must handle validation errors before writing, call it explicitly:

from django.core.exceptions import ValidationError

article = Article(title="", body="Draft")
try:
    article.full_clean()
except ValidationError as exc:
    # exc.message_dict maps fields to lists of messages
    handle_validation_errors(exc.message_dict)
else:
    article.save()

Use full_clean() when you own the validation boundary—for example, service code importing data, background jobs, or instances assembled outside a ModelForm. A ModelForm already applies model checks for its included fields. Validation is not a substitute for database transactions or unique constraints: concurrent writes can still race, so enforce critical invariants at the database layer and handle the resulting integrity error.

Where each rule belongs

Rule type Best location Error location
Required input, type conversion, one reusable predicate Field options or validators=[...] That field
One field with custom normalization or context clean_<fieldname>() That field
Relationship between form fields Form clean() Non-field error or add_error() target
Rules that must hold for model instances everywhere Model clean(), model fields, constraints Model ValidationError
Uniqueness and database integrity Model validation plus database constraint Form field/non-field error or database exception

Common mistakes and fixes

Reading raw POST data

Symptom: dates, numbers, or booleans behave like strings. Fix: call is_valid() and use cleaned_data; field cleaning performs conversion.

Using cleaned_data before validation

Symptom: missing keys or stale values. Fix: access it only inside the successful branch, and use .get() inside cleaning methods because earlier field errors can remove keys.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Putting a cross-field rule in one field validator

Symptom: a validator cannot reliably see the other input. Fix: move the relationship check to clean().

Forgetting super().clean() in a ModelForm

Symptom: expected uniqueness errors disappear. Fix: call the parent method and modify the returned dictionary.

Expecting save() to validate

Symptom: a manually built instance reaches the database with invalid values. Fix: call instance.full_clean() before save(), while retaining database constraints for concurrency safety.

Missing uploaded files

Symptom: a file field is empty despite a multipart form. Fix: bind both request.POST and request.FILES, and use enctype="multipart/form-data" in HTML.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Testing validation behavior

Test valid normalization, each invalid branch, error placement, and model uniqueness. Assert both the Boolean result and the resulting data or message.

from django.test import TestCase
from .forms import RegistrationForm

class RegistrationFormTests(TestCase):
    def test_dates_are_normalized(self):
        form = RegistrationForm(data={
            "password": "secret",
            "password_again": "secret",
            "start_date": "2026-09-29",
        })
        self.assertTrue(form.is_valid())
        self.assertEqual(form.cleaned_data["start_date"].isoformat(), "2026-09-29")

    def test_password_mismatch_is_field_error(self):
        form = RegistrationForm(data={
            "password": "one",
            "password_again": "two",
            "start_date": "2026-09-29",
        })
        self.assertFalse(form.is_valid())
        self.assertIn("password_again", form.errors)

Pin expectations to the Django version used by your project. Validation details and available constraint behavior can vary between supported releases, so verify against that version’s documentation and test suite.

Performance, reliability and cost considerations

Form validation is normally inexpensive, but ModelForms can issue database queries for uniqueness checks. Avoid calling is_valid() repeatedly in one request; validation is cached, but a single explicit call makes control flow clearer. Keep expensive external checks out of field cleaning when possible, or cache them and provide a timeout and failure path. Validation should produce actionable errors, never expose secrets or internal exception details.

For bulk imports, validate rows in batches, report row-level errors, and wrap writes in transactions where partial updates would be harmful. Database constraints remain the final protection against race conditions even when every form passes validation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Or skip the browser setup

If your Django project also needs reliable screenshots of rendered forms, ScreenshotNeo provides a single HTTP request instead of maintaining a browser automation stack. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.

One-call example (see the ScreenshotNeo API documentation):

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Frequently Asked Questions

Does calling form.is_valid() save anything to the database?

No. It only runs validation and populates errors and cleaned data. A separate save() call is required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I validate a form without a request object?

Yes. Pass a dictionary with Form(data=...) in application code or tests; use request.POST and request.FILES in a view.

Should validation be duplicated in JavaScript?

Client-side checks can improve feedback, but repeat every security and business rule on the server because browsers can bypass JavaScript.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.