Once you already have a model, do not write a form by hand and map every field to the model yourself. Django’s ModelForm generates the form from the model, validates it against exactly the field constraints, and saves it straight to an object with a single form.save(). It fits best for CRUD forms — creating posts, editing profiles, entering data — where the form fields almost entirely match the model fields.
The Problem
Suppose you have an Article model and want a page to create posts. The hand-written way usually looks like this:
def article_create(request): if request.method == "POST": title = request.POST.get("title", "") content = request.POST.get("content", "") # Không có validate: title rỗng vẫn lưu được, # max_length=200 cũng không ai chặn giúp bạn Article.objects.create(title=title, content=content) return redirect("article-list") return render(request, "blog/article_form.html")
The problem is that every model constraint — required, max length, data type — is ignored. You have to write validation yourself, re-render the form with errors when it fails, and every time you add a field to the model you have to edit yet another place.
The Solution
ModelForm does all three jobs in one class. Suppose your model looks like this:
# blog/models.pyfrom django.db import modelsclass Article(models.Model): title = models.CharField(max_length=200) summary = models.CharField(max_length=300, blank=True) content = models.TextField()
Declare the model, list the fields, and you are done:
# blog/forms.pyfrom django import formsfrom .models import Articleclass ArticleForm(forms.ModelForm): class Meta: model = Article fields = ["title", "summary", "content"] widgets = { "summary": forms.Textarea(attrs={"rows": 3}), } def clean_title(self): title = self.cleaned_data["title"] if len(title) < 10: raise forms.ValidationError("Tiêu đề phải dài ít nhất 10 ký tự.") return title
The view shrinks to a few lines, and you still keep control before saving:
# blog/views.pyfrom django.shortcuts import render, redirectfrom .forms import ArticleFormdef article_create(request): if request.method == "POST": form = ArticleForm(request.POST) if form.is_valid(): article = form.save(commit=False) article.summary = article.summary or "Chưa có tóm tắt" article.save() return redirect("article-list") else: form = ArticleForm() return render(request, "blog/article_form.html", {"form": form})
In the template, just {{ form.as_p }} plus {% csrf_token %} and the form renders fully, with per-field errors when input is wrong.
Why this is better:
- Free validation: form.is_valid() checks exactly the model constraints, from required fields to max_length, instead of you writing it by hand.
- Less duplication: add a field to the model and you only add its name to the fields list.
- Errors shown out of the box: an invalid form re-renders with per-field errors, with no need to pass errors yourself.
- Customizable when needed: clean_<field> for per-field validation, widgets to change the input UI, commit=False to attach extra data before the real save.
Apply It to Your Project
Use ModelForm for every CRUD form that closely follows a model: post create/edit forms, user profiles, internal order forms. When the form diverges far from the model — for example a signup form spanning several models, or a search-only form — that is when you go back to a plain forms.Form. One small but important rule: list fields explicitly, never use fields = “__all__”, so a sensitive field such as is_staff is not accidentally exposed when the model changes later.
Related Posts
- Django: Block Invalid Data at the Model Level with clean() and full_clean() in 5 Minutes
- Django: Fix N+1 Queries with select_related in 5 Minutes
- Django: Compute on QuerySets with annotate() Instead of Python Loops
FAQ
How is ModelForm different from forms.Form?
A plain form is not tied to any model: you define the fields yourself and handle saving yourself. A ModelForm generates fields from the model and comes with a save() method.
When should you use fields = “__all__”?
Almost never. List each field explicitly so that when the model gains a sensitive field, the form does not automatically expose it.
How do you validate across multiple fields?
Write a form-wide clean() method, read the values from self.cleaned_data, and raise ValidationError when the combination is invalid.
What about a form with file uploads?
Pass request.FILES to the form as well when instantiating it, and add enctype=”multipart/form-data” to the form tag in the template.
Can ModelForm be used for edit forms?
Yes. Pass instance=article to the form, and it will pre-fill the old data and update exactly that object on save instead of creating a new one.
💬 Have you tried this in your project? If you hit an error or know a cleaner way, leave a comment below — I read them all and will add the best ideas to this post.
References
- Django documentation, “Creating forms from models”: https://docs.djangoproject.com/en/6.1/topics/forms/modelforms/
- Django documentation, “Form and field validation”: https://docs.djangoproject.com/en/6.1/ref/forms/validation/
