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

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 include

You control the included fields three ways inside Meta:

# 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? True

3 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

CodeWhat It Does
class PostForm(forms.ModelForm)Declare a ModelForm
model = PostTell 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

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.