Skip to content

Django Forms#

Complete guide to Django Forms and ModelForms for validation and data handling.

🎯 Basic Form#

Define Form#

# myapp/forms.py
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100, label='Your Name')
    email = forms.EmailField(label='Your Email')
    message = forms.CharField(
        widget=forms.Textarea,
        label='Message',
        help_text='Enter your message here'
    )
    age = forms.IntegerField(min_value=0, max_value=120, required=False)
    website = forms.URLField(required=False)
    agree = forms.BooleanField(label='I agree to terms')

Use Form in View#

# myapp/views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import ContactForm

def contact_view(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            # Access cleaned data
            name = form.cleaned_data['name']
            email = form.cleaned_data['email']
            message = form.cleaned_data['message']

            # Process form data
            # Send email, save to database, etc.

            messages.success(request, 'Message sent!')
            return redirect('myapp:contact')
    else:
        form = ContactForm()

    return render(request, 'myapp/contact.html', {'form': form})

Render Form in Template#

<!-- myapp/templates/myapp/contact.html -->
{% extends 'myapp/base.html' %}

{% block content %}
<h1>Contact Us</h1>

<form method="post">
    {% csrf_token %}

    <!-- Render entire form -->
    {{ form.as_p }}

    <!-- Or render fields individually -->
    <div>
        {{ form.name.label_tag }}
        {{ form.name }}
        {{ form.name.errors }}
        {% if form.name.help_text %}
            <small>{{ form.name.help_text }}</small>
        {% endif %}
    </div>

    <div>
        {{ form.email.label_tag }}
        {{ form.email }}
        {{ form.email.errors }}
    </div>

    <div>
        {{ form.message.label_tag }}
        {{ form.message }}
        {{ form.message.errors }}
    </div>

    <button type="submit">Send</button>
</form>
{% endblock %}

📋 Form Rendering Options#

<!-- Render as paragraphs -->
{{ form.as_p }}

<!-- Render as table -->
{{ form.as_table }}

<!-- Render as list -->
{{ form.as_ul }}

<!-- Render fields individually -->
{{ form.name }}
{{ form.email }}
{{ form.message }}

🎨 Form Fields#

Text Fields#

# CharField
name = forms.CharField(
    max_length=100,
    min_length=2,
    required=True,
    label='Name',
    help_text='Enter your full name',
    initial='John Doe',
    widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Enter name'})
)

# TextField (Textarea)
message = forms.CharField(
    widget=forms.Textarea(attrs={'rows': 5, 'cols': 40, 'class': 'form-control'}),
    max_length=1000
)

# EmailField
email = forms.EmailField()

# URLField
website = forms.URLField()

# SlugField
slug = forms.SlugField()

Numeric Fields#

# IntegerField
age = forms.IntegerField(min_value=0, max_value=120)

# DecimalField
price = forms.DecimalField(max_digits=10, decimal_places=2)

# FloatField
rating = forms.FloatField(min_value=0.0, max_value=5.0)

Choice Fields#

# ChoiceField
STATUS_CHOICES = [
    ('draft', 'Draft'),
    ('published', 'Published'),
]

status = forms.ChoiceField(choices=STATUS_CHOICES)

# RadioSelect widget
status = forms.ChoiceField(
    choices=STATUS_CHOICES,
    widget=forms.RadioSelect
)

# TypedChoiceField (with type coercion)
priority = forms.TypedChoiceField(
    choices=[(1, 'Low'), (2, 'Medium'), (3, 'High')],
    coerce=int
)

Date/Time Fields#

# DateField
birth_date = forms.DateField(
    widget=forms.DateInput(attrs={'type': 'date'})
)

# DateTimeField
event_date = forms.DateTimeField(
    widget=forms.DateTimeInput(attrs={'type': 'datetime-local'})
)

# TimeField
start_time = forms.TimeField(
    widget=forms.TimeInput(attrs={'type': 'time'})
)

File Fields#

# FileField
document = forms.FileField()

# ImageField (requires Pillow)
photo = forms.ImageField()

# Multiple files
files = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}))

Boolean Fields#

# BooleanField
agree = forms.BooleanField(label='I agree to terms')

# NullBooleanField
is_featured = forms.NullBooleanField()

🎯 ModelForm#

Basic ModelForm#

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

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content', 'author', 'category', 'tags', 'is_published']
        # Or exclude specific fields
        # exclude = ['slug', 'views', 'created_at']

        # Customize widgets
        widgets = {
            'content': forms.Textarea(attrs={'rows': 10, 'class': 'form-control'}),
            'published_at': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
        }

        # Customize labels
        labels = {
            'title': 'Post Title',
            'content': 'Post Content',
        }

        # Customize help text
        help_texts = {
            'title': 'Enter a descriptive title',
            'content': 'Write your post content here',
        }

Use ModelForm in View#

# myapp/views.py
from django.shortcuts import render, redirect, get_object_or_404
from .forms import PostForm
from .models import Post

def post_create(request):
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            form.save_m2m()  # Save ManyToMany fields
            return redirect('myapp:post_detail', pk=post.pk)
    else:
        form = PostForm()

    return render(request, 'myapp/post_form.html', {'form': form})

def post_update(request, pk):
    post = get_object_or_404(Post, pk=pk)

    if request.method == 'POST':
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            form.save()
            return redirect('myapp:post_detail', pk=post.pk)
    else:
        form = PostForm(instance=post)

    return render(request, 'myapp/post_form.html', {'form': form, 'post': post})

✅ Custom Validation#

Field-Level Validation#

# myapp/forms.py
from django import forms
from django.core.exceptions import ValidationError

class PostForm(forms.ModelForm):
    title = forms.CharField(max_length=200)

    def clean_title(self):
        title = self.cleaned_data.get('title')
        if title and len(title) < 10:
            raise ValidationError('Title must be at least 10 characters long.')
        return title

    class Meta:
        model = Post
        fields = ['title', 'content']

Form-Level Validation#

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content', 'excerpt']

    def clean(self):
        cleaned_data = super().clean()
        title = cleaned_data.get('title')
        content = cleaned_data.get('content')

        # Cross-field validation
        if title and content:
            if title.lower() in content.lower():
                raise ValidationError('Title should not be repeated in content.')

        return cleaned_data

Custom Validators#

# myapp/validators.py
from django.core.exceptions import ValidationError

def validate_no_profanity(value):
    profanity_words = ['bad', 'word']  # Example
    if any(word in value.lower() for word in profanity_words):
        raise ValidationError('Please avoid inappropriate language.')

# Use in form
class PostForm(forms.ModelForm):
    content = forms.CharField(validators=[validate_no_profanity])

    class Meta:
        model = Post
        fields = ['title', 'content']

🎨 Custom Widgets#

Custom Widget#

# myapp/widgets.py
from django import forms

class CustomTextarea(forms.Textarea):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.attrs.update({'class': 'custom-textarea', 'rows': 10})

# Use in form
class PostForm(forms.ModelForm):
    content = forms.CharField(widget=CustomTextarea())

    class Meta:
        model = Post
        fields = ['title', 'content']

📝 FormSets#

ModelFormSet#

# myapp/forms.py
from django.forms import modelformset_factory
from .models import Post

PostFormSet = modelformset_factory(
    Post,
    fields=['title', 'content'],
    extra=2,  # Number of empty forms
    can_delete=True  # Allow deletion
)

# In view
def manage_posts(request):
    if request.method == 'POST':
        formset = PostFormSet(request.POST)
        if formset.is_valid():
            formset.save()
            return redirect('myapp:post_list')
    else:
        formset = PostFormSet(queryset=Post.objects.filter(author=request.user))

    return render(request, 'myapp/manage_posts.html', {'formset': formset})
<!-- myapp/templates/myapp/manage_posts.html -->
<form method="post">
    {% csrf_token %}
    {{ formset.management_form }}

    {% for form in formset %}
        <div class="form-row">
            {{ form.id }}
            {{ form.title }}
            {{ form.content }}
            {% if form.DELETE %}
                {{ form.DELETE }} Delete
            {% endif %}
        </div>
    {% endfor %}

    <button type="submit">Save</button>
</form>

🎯 Inline Formsets#

# myapp/forms.py
from django.forms import inlineformset_factory
from .models import Post, Comment

CommentFormSet = inlineformset_factory(
    Post,
    Comment,
    fields=['author', 'content'],
    extra=1,
    can_delete=True
)

# In view
def edit_post_with_comments(request, pk):
    post = get_object_or_404(Post, pk=pk)

    if request.method == 'POST':
        form = PostForm(request.POST, instance=post)
        formset = CommentFormSet(request.POST, instance=post)

        if form.is_valid() and formset.is_valid():
            form.save()
            formset.save()
            return redirect('myapp:post_detail', pk=post.pk)
    else:
        form = PostForm(instance=post)
        formset = CommentFormSet(instance=post)

    return render(request, 'myapp/edit_post.html', {
        'form': form,
        'formset': formset
    })

✅ Best Practices#

  1. Use ModelForm - For model-based forms
  2. Validate in forms - Not just in views
  3. Use cleaned_data - Always use cleaned_data, not request.POST
  4. Customize widgets - Better UX with proper widgets
  5. Handle errors - Display form errors in templates
  6. Use commit=False - When you need to set additional fields
  7. Save ManyToMany separately - Use form.save_m2m() after commit=False

✅ Complete Example#

# myapp/forms.py
from django import forms
from django.core.exceptions import ValidationError
from .models import Post, Category

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'slug', 'content', 'excerpt', 'category', 'tags', 
                  'is_published', 'published_at']
        widgets = {
            'content': forms.Textarea(attrs={'rows': 15, 'class': 'form-control'}),
            'excerpt': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
            'published_at': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
            'tags': forms.CheckboxSelectMultiple,
        }
        labels = {
            'title': 'Post Title',
            'slug': 'URL Slug',
        }
        help_texts = {
            'slug': 'URL-friendly version of title',
        }

    def clean_title(self):
        title = self.cleaned_data.get('title')
        if title and len(title) < 10:
            raise ValidationError('Title must be at least 10 characters.')
        return title

    def clean(self):
        cleaned_data = super().clean()
        is_published = cleaned_data.get('is_published')
        published_at = cleaned_data.get('published_at')

        if is_published and not published_at:
            from django.utils import timezone
            cleaned_data['published_at'] = timezone.now()

        return cleaned_data

✅ Next Steps#


Pro Tip: Always use form.cleaned_data instead of request.POST - it's validated and safe. Use commit=False when you need to set additional fields before saving!