ModelForms

Difficulty: Intermediate

ModelForm is a Django form that is automatically generated from a model. Instead of manually defining form fields that mirror your model fields, ModelForm introspects the model and creates the corresponding form fields automatically. This eliminates duplication and ensures that form validation stays in sync with model constraints.

To create a ModelForm, you subclass forms.ModelForm and define an inner Meta class that specifies the model and which fields to include. You can use 'fields' to explicitly list included fields or '__all__' to include everything. Alternatively, 'exclude' lists fields to omit. Using 'fields' with explicit field names is recommended over '__all__' for security - it prevents accidentally exposing sensitive fields if you add new fields to the model later.

ModelForm automatically maps model field types to form field types. CharField becomes forms.CharField, TextField becomes forms.CharField with Textarea widget, ForeignKey becomes forms.ModelChoiceField with a select dropdown, ManyToManyField becomes forms.ModelMultipleChoiceField, BooleanField becomes forms.BooleanField, and so on. It also inherits model-level constraints like max_length, blank, null, choices, and validators.

The save() method on ModelForm creates or updates a model instance. When called without arguments, it saves directly to the database. With commit=False, it returns an unsaved model instance, allowing you to modify fields before saving. This is commonly used to set fields that are not part of the form, like setting the author to the current user.

You can customize ModelForm fields by redefining them in the form class or by using the widgets, labels, help_texts, and error_messages dictionaries in the Meta class. This lets you change widgets, add CSS classes, or customize labels without rewriting the entire field definition.

ModelForm also supports instance binding for editing existing objects. Pass an existing model instance via the 'instance' keyword argument to pre-fill the form with the object's current data. When saved, the form updates the existing instance rather than creating a new one.

Code examples

Basic ModelForm

from django import forms
from .models import Article

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ['title', 'content', 'category', 'tags', 'is_published']
        widgets = {
            'title': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Article title',
            }),
            'content': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 10,
            }),
        }
        labels = {
            'is_published': 'Publish immediately',
        }
        help_texts = {
            'tags': 'Select one or more tags.',
        }

The Meta class configures the ModelForm. fields lists which model fields to include. widgets, labels, and help_texts customize individual fields without redefining them. This form auto-generates from the Article model.

Using ModelForm in Views

from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from .models import Article
from .forms import ArticleForm

@login_required
def article_create(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user  # Set non-form field
            article.save()
            form.save_m2m()  # Save ManyToMany relationships
            return redirect('blog:article-detail', pk=article.pk)
    else:
        form = ArticleForm()
    return render(request, 'blog/article_form.html', {'form': form})

@login_required
def article_edit(request, pk):
    article = get_object_or_404(Article, pk=pk, author=request.user)
    if request.method == 'POST':
        form = ArticleForm(request.POST, instance=article)
        if form.is_valid():
            form.save()
            return redirect('blog:article-detail', pk=article.pk)
    else:
        form = ArticleForm(instance=article)  # Pre-fill with existing data
    return render(request, 'blog/article_form.html', {
        'form': form,
        'article': article,
    })

commit=False returns an unsaved instance so you can set the author. When using commit=False with M2M fields, you must call form.save_m2m() separately. For editing, pass instance=article to pre-fill and update the existing object.

Overriding ModelForm Fields

from django import forms
from .models import Product

class ProductForm(forms.ModelForm):
    # Override a field completely
    price = forms.DecimalField(
        min_value=0.01,
        max_digits=10,
        decimal_places=2,
        widget=forms.NumberInput(attrs={
            'class': 'form-control',
            'step': '0.01',
        }),
    )

    # Add a field that is not on the model
    notify_subscribers = forms.BooleanField(
        required=False,
        label='Notify subscribers about this product',
    )

    class Meta:
        model = Product
        fields = ['name', 'description', 'price', 'category', 'image']

    def save(self, commit=True):
        product = super().save(commit=commit)
        if self.cleaned_data.get('notify_subscribers'):
            # Custom logic for non-model field
            pass  # Send notification
        return product

You can override model fields by redefining them in the form class. You can also add extra fields not on the model. Override save() to handle non-model fields or add custom post-save logic.

fields vs exclude

from django import forms
from .models import UserProfile

# RECOMMENDED: Explicit field list (whitelist approach)
class ProfileForm(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = ['bio', 'avatar', 'website', 'phone']

# DISCOURAGED: Exclude approach (blacklist)
class ProfileFormExclude(forms.ModelForm):
    class Meta:
        model = UserProfile
        exclude = ['user', 'is_verified', 'created_at']

# DANGEROUS: Include everything
class ProfileFormAll(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = '__all__'

# Why 'fields' is safer:
# If you add a sensitive field to the model later (e.g., is_admin),
# - 'fields' list: new field is NOT included (safe)
# - 'exclude' list: new field IS included unless you remember to exclude it
# - '__all__': new field IS included (potentially dangerous)

Always prefer the explicit fields list over exclude or __all__. The whitelist approach ensures that new model fields are not accidentally exposed in forms, which is especially important for security-sensitive fields.

Key points

Concepts covered

ModelForm, Meta, fields, exclude, save, commit