ModelForms

Django · Practical · 12 min

Most forms edit model instances; ModelForm removes the duplication between model and form.

A `ModelForm` builds its fields from a model:

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["title", "slug", "body", "tags"]

Field types, max_length, choices, blank and help text come from the model. Validation runs both the form's checks and the model's (full_clean), including unique constraints.

Saving:

  • form.save() inserts a new row or updates the instance you passed in
  • edit an existing object with PostForm(request.POST, instance=post)
  • form.save(commit=False) returns an unsaved instance so you can set fields that aren't in the form, like obj.author = request.user, then call obj.save(). With commit=False and many-to-many fields, call form.save_m2m() afterwards.

List fields explicitly. fields = "__all__" or exclude = [...] means any new model field automatically becomes editable by users, a mass-assignment vulnerability waiting to happen (is_staff, price, owner...).

Customize with widgets = {"body": forms.Textarea(attrs={"rows": 10})}, labels, help_texts, or by overriding a field declaratively on the form class. Add clean_<field> methods as with normal forms.

Generic views CreateView and UpdateView use ModelForms automatically, which is why they need only model and fields to work.

Example

# blog/forms.py
from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["title", "slug", "body", "tags"]     # explicit, never "__all__"
        widgets = {"body": forms.Textarea(attrs={"rows": 12})}

# blog/views.py
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render

@login_required
def post_edit(request, pk=None):
    post = get_object_or_404(Post, pk=pk) if pk else None
    form = PostForm(request.POST or None, instance=post)
    if request.method == "POST" and form.is_valid():
        obj = form.save(commit=False)
        obj.author = request.user.author       # field not in the form
        obj.save()
        form.save_m2m()                        # needed after commit=False
        return redirect("blog:detail", pk=obj.pk)
    return render(request, "blog/post_form.html", {"form": form})

Remember

  • ModelForm derives fields and validation from the model
  • instance= edits an existing row; save() creates or updates
  • save(commit=False) lets you set extra fields; then save_m2m()
  • Always list fields explicitly

Watch out for

  • Using fields = "__all__", exposing sensitive fields added later.

Check yourself

  1. You call form.save(commit=False), set obj.author and call obj.save(). The form has a ManyToMany field. What else is needed?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.djangoproject.com/en/6.0/topics/forms/modelforms/