ModelForms
Reviewed & published by Brayan K
A Django ModelForm is a form class built automatically from a model — it inspects the model's fields, generates matching form fields and validation, and can save submitted data straight back to the database with a single save() call.
Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll define a ModelForm with its inner Meta class, use it in a view to create records, set extra fields before saving, and reuse the same form to edit existing objects.
What You'll Learn in This Lesson
- What a ModelForm is and when to use one
- Defining a ModelForm with the inner Meta class
- Choosing fields with a list, '__all__', or exclude
- Saving a new record with form.save()
- Setting extra fields with save(commit=False)
- Editing an object with ModelForm(instance=obj)
1 Defining a ModelForm
A ModelForm is a class that subclasses forms.ModelForm and contains an inner class called Meta. The Meta class tells Django two things: which model the form is built from, and which fields of that model to include.
Say you have a Post model. The ModelForm below reads its title and body columns and generates the matching form fields, complete with the validation Django infers from each model field:
# blog/models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
author = models.CharField(max_length=100)
# blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post # which model to build from
fields = ["title", "body"] # which fields to includeYou control the included fields three ways inside Meta:
- fields = ["title", "body"] — an explicit list (recommended).
- fields = '__all__' — include every field on the model.
- exclude = ["author"] — include everything except the listed names.
# A ModelForm reads its field list from the model. This simulates the
# mechanic: the form "inspects" a model and keeps the fields you asked for.
model_fields = ["title", "body", "author"] # fields on the Post model
class Meta:
fields = ["title", "body"] # what PostForm includes
# Django builds one form field per name in Meta.fields:
form_fields = [name for name in model_fields if name in Meta.fields]
print("Form will render these inputs:", form_fields)
# Using '__all__' would render every model field instead:
print("With '__all__' it would render:", model_fields)
# ✅ Expected output:
# Form will render these inputs: ['title', 'body']
# With '__all__' it would render: ['title', 'body', 'author']2 Saving a Record in a View
The big payoff of a ModelForm is form.save(). When the submitted data is valid, one call writes a new row to the database. A typical create view checks request.method, validates with is_valid(), then saves:
# blog/views.py
from django.shortcuts import render, redirect
from .forms import PostForm
def create_post(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
form.save() # one call writes the new Post row
return redirect("post_list")
else:
form = PostForm() # empty form on a GET request
return render(request, "blog/post_form.html", {"form": form})Sometimes the model needs a value the user shouldn't supply — like the author, which should be the logged-in user. Use save(commit=False) to build the object without hitting the database, set the extra field, then call save() yourself:
def create_post(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False) # build, but don't save yet
post.author = request.user.username # set the extra field
post.save() # NOW write it to the database
return redirect("post_list")
else:
form = PostForm()
return render(request, "blog/post_form.html", {"form": form})# This models the save(commit=False) pattern in plain Python.
# A ModelForm builds an object from cleaned data; with commit=False
# we get the object back, set an extra field, then persist it.
class Post:
def __init__(self, title, body):
self.title = title
self.body = body
self.author = None
self.saved = False
def save(self):
self.saved = True
print(f"Saved Post: '{self.title}' by {self.author}")
# form.save(commit=False) -> unsaved object with the user's data filled in
post = Post(title="Hello Django", body="My first post")
print("Saved yet?", post.saved) # False — not in the database
post.author = "alice" # set the field the form didn't collect
post.save() # now it's written
print("Saved yet?", post.saved) # True
# ✅ Expected output:
# Saved yet? False
# Saved Post: 'Hello Django' by alice
# Saved yet? True3 Editing Objects & Customising Fields
To edit an existing record, pass it as the instance argument. On a GET request the form renders pre-filled with the object's current values; on a POST you pass both the data and the instance so save() updates that row instead of creating a new one:
# blog/views.py
from django.shortcuts import render, redirect, get_object_or_404
from .models import Post
from .forms import PostForm
def edit_post(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == "POST":
form = PostForm(request.POST, instance=post) # update this row
if form.is_valid():
form.save()
return redirect("post_list")
else:
form = PostForm(instance=post) # pre-filled with current values
return render(request, "blog/post_form.html", {"form": form})You can also customise how fields render right inside Meta using widgets and labels. For example, make the body a larger textarea and give the title a friendlier label:
# blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "body"]
widgets = {
"body": forms.Textarea(attrs={"rows": 6, "cols": 40}),
}
labels = {
"title": "Post headline",
}
help_texts = {
"title": "Keep it under 200 characters.",
}# Editing reuses the SAME form class — the only difference is whether
# you pass an existing instance. This simulates create vs. edit.
def post_form(data, instance=None):
"""Returns the values the form will work with."""
if instance is None:
# create: start from the submitted data only
return {"mode": "create", **data}
else:
# edit: start from the instance, then apply any new data
result = dict(instance)
result.update(data)
result["mode"] = "edit"
return result
# Create: no instance, just new data
print(post_form({"title": "New Post", "body": "..."}))
# Edit: an existing row is passed as instance and updated
existing = {"title": "Old Title", "body": "old body"}
print(post_form({"title": "Updated Title"}, instance=existing))
# ✅ Expected output:
# {'mode': 'create', 'title': 'New Post', 'body': '...'}
# {'title': 'Updated Title', 'body': 'old body', 'mode': 'edit'}🎯 Your Turn: Complete the Meta Class
Fill in the blanks so the helper reports the correct model and fields for a ModelForm built from the Post model that shows only title and body.
# 🎯 YOUR TURN: replace the ___ blanks.
# The Meta class needs the model name "Post" and the two fields to show.
def describe_modelform(meta):
return f"ModelForm for {meta['model']} showing {meta['fields']}"
meta = {
"model": "___", # which model? (hint: "Post")
"fields": ["___", "___"], # which two fields? (hint: title, body)
}
print(describe_modelform(meta))
# EXPECTED OUTPUT:
# ModelForm for Post showing ['title', 'body']Common Errors (And How to Fix Them)
❌ ValueError: ModelForm has no model class specified
You forgot the inner Meta class, or it doesn't set model.
✅ Fix: add class Meta: with model = Post inside your form.
❌ The form renders blank — no inputs appear
No fields were listed, so Django generated a form with nothing on it.
✅ Fix: set fields = ["title", "body"] or fields = '__all__' in Meta.
❌ The record never gets saved
You called form.save(commit=False) and then forgot the final .save().
✅ Fix: after setting your extra fields, call post.save() on the returned object.
📋 Quick Reference
| Code | What It Does |
|---|---|
| class PostForm(forms.ModelForm) | Declare a ModelForm |
| model = Post | Tell Meta which model to build from |
| fields = '__all__' | Include every model field |
| form.save() | Write a new row to the database |
| form.save(commit=False) | Build the object without saving it yet |
| PostForm(instance=obj) | Edit an existing object |
🏆 Mini Challenge: A Create-Then-Edit Flow
Model the full lifecycle: create a post with save(commit=False) to attach an author, then edit it through the same form using an instance.
# 🏆 Mini Challenge: create a record, then edit it.
class Post:
def __init__(self, title, body, author=None):
self.title = title
self.body = body
self.author = author
def __repr__(self):
return f"Post(title={self.title!r}, author={self.author!r})"
def save_new(title, body, author):
"""Like form.save(commit=False) then setting author and saving."""
post = Post(title, body) # built from form data
post.author = author # extra field set in the view
return post # would be post.save() in Django
def edit(post, **changes):
"""Like PostForm(request.POST, instance=post).save()."""
for field, value in changes.items():
setattr(post, field, value)
return post
p = save_new("Django Forms", "Draft body", author="alice")
print("Created:", p)
p = edit(p, title="Django ModelForms")
print("Edited: ", p)
# EXPECTED:
# Created: Post(title='Django Forms', author='alice')
# Edited: Post(title='Django ModelForms', author='alice')❓ Frequently Asked Questions
Lesson complete — you can build forms straight from your models!
You now know how to define a ModelForm with its inner Meta class, choose its fields, save new records with save(), set extra fields with save(commit=False), and edit existing objects by passing an instance.
Practice quiz
What is a ModelForm built from?
- A URL pattern
- A model
- A template
- A migration
Answer: A model. A ModelForm is generated automatically from a model.
Which base class does a ModelForm subclass?
- forms.Form
- forms.ModelForm
- models.Model
- forms.BaseForm
Answer: forms.ModelForm. A ModelForm subclasses forms.ModelForm.
Where do you specify the model and fields for a ModelForm?
- In the view
- In a settings file
- In the inner Meta class
- In urls.py
Answer: In the inner Meta class. The inner Meta class sets model and fields.
Which Meta option includes every model field?
- fields = '__all__'
- fields = 'every'
- include = 'all'
- fields = []
Answer: fields = '__all__'. fields = '__all__' includes every field on the model.
Why is an explicit fields list recommended over '__all__'?
- It is faster
- It prevents new model fields from silently appearing on the form
- It is required
- It enables validation
Answer: It prevents new model fields from silently appearing on the form. An explicit list keeps newly added fields off the form automatically.
What does form.save() do when data is valid?
- Renders the template
- Validates only
- Deletes the row
- Writes a row to the database
Answer: Writes a row to the database. form.save() writes the row when the form is valid.
What does form.save(commit=False) return?
- A boolean
- The form HTML
- An unsaved model instance
- A QuerySet
Answer: An unsaved model instance. It returns an unsaved object so you can set extra fields first.
After save(commit=False) and setting extra fields, what must you do?
- Nothing, it auto-saves
- Call .save() on the returned object
- Call form.clean()
- Redirect immediately
Answer: Call .save() on the returned object. You must call .save() on the object to persist it.
How do you edit an existing record with a ModelForm?
- Pass it as the instance argument
- Use a new form class
- Call update()
- Set fields = 'edit'
Answer: Pass it as the instance argument. Pass the object via instance so save() updates that row.
Which Meta option customises how a field renders, like a larger textarea?
- labels
- help_texts
- widgets
- fields
Answer: widgets. widgets lets you set, e.g., forms.Textarea for a field.
Continue this course
- Previous: Forms
- Next: Class-Based Views — Write reusable, object-oriented views and understand the request flow
- Quick reference: Django cheat sheet › Forms
Frequently asked questions
What is the difference between a Form and a ModelForm in Django?
A plain Form (forms.Form) makes you declare every field by hand and write your own save logic. A ModelForm is built automatically from a model: it reads the model's fields, generates matching form fields with validation, and gives you a save() method that writes a row to the database. Use a ModelForm whenever the form maps onto a model; use a plain Form for a contact form or search box with no model behind it.
What does form.save(commit=False) do?
It builds a model instance from the cleaned form data but does NOT write it to the database yet. It returns the unsaved object so you can set extra attributes — such as the logged-in user as the author — before you call .save() yourself. This is the standard pattern when a model has required fields that are not collected from the user.
How do I edit an existing record with a ModelForm?
Pass the object as the instance argument: ModelForm(instance=obj). On a GET the form renders pre-filled with that object's values, and on a POST you call ModelForm(request.POST, instance=obj) so save() updates the existing row instead of creating a new one. The same form class handles both create and edit.
Can I choose which model fields appear on the form?
Yes. Inside the inner Meta class set fields to a list of names, or use fields = '__all__' for every model field, or use exclude to leave specific fields out. Listing fields explicitly is recommended because it prevents new model fields from silently appearing on your form later.