Django forms cheatsheet.

Form

from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea)
    subscribe = forms.BooleanField(required=False)

ModelForm

from .models import Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["title", "body", "tags"]
        # OR exclude:
        # exclude = ["author"]
        widgets = {
            "body": forms.Textarea(attrs={"rows": 10}),
        }
        labels = { "body": "Content" }
        help_texts = { "title": "Be specific" }

Usage in view

def post_create(request):
    if request.method == "POST":
        form = PostForm(request.POST, request.FILES)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            form.save_m2m()        # if commit=False
            return redirect("blog:list")
    else:
        form = PostForm()
    return render(request, "blog/form.html", {"form": form})

Validation: clean methods

class SignupForm(forms.Form):
    password = forms.CharField(widget=forms.PasswordInput)
    confirm = forms.CharField(widget=forms.PasswordInput)
    
    def clean_password(self):
        pw = self.cleaned_data["password"]
        if len(pw) < 8:
            raise forms.ValidationError("Too short")
        return pw
    
    def clean(self):
        cleaned = super().clean()
        if cleaned.get("password") != cleaned.get("confirm"):
            raise forms.ValidationError("Passwords don't match")
        return cleaned

Common widgets

forms.TextInput
forms.Textarea
forms.PasswordInput
forms.CheckboxInput
forms.RadioSelect
forms.Select
forms.SelectMultiple
forms.CheckboxSelectMultiple
forms.DateInput(attrs={"type": "date"})
forms.DateTimeInput(attrs={"type": "datetime-local"})
forms.FileInput
forms.ClearableFileInput
forms.HiddenInput
forms.NumberInput
forms.EmailInput
forms.URLInput

Widget attrs

title = forms.CharField(
    widget=forms.TextInput(attrs={
        "class": "form-input",
        "placeholder": "Title…",
        "autofocus": True,
    })
)

Field types

forms.CharField(max_length=200, min_length=1, required=True)
forms.EmailField()
forms.URLField()
forms.IntegerField(min_value=0, max_value=100)
forms.DecimalField(max_digits=10, decimal_places=2)
forms.DateField()
forms.DateTimeField()
forms.TimeField()
forms.FileField()
forms.ImageField()
forms.BooleanField()
forms.ChoiceField(choices=[("a", "A"), ("b", "B")])
forms.MultipleChoiceField(choices=[...])
forms.ModelChoiceField(queryset=Category.objects.all())
forms.ModelMultipleChoiceField(queryset=Tag.objects.all())
forms.SlugField()
forms.UUIDField()
forms.JSONField()

Rendering forms

<form method="post" enctype="multipart/form-data">
  {% csrf_token %}
  {{ form }}
  <button>Save</button>
</form>

Per-field:

{{ form.title.label_tag }}
{{ form.title }}
{{ form.title.errors }}

<!-- Or iterate -->
{% for field in form %}
  <div>
    {{ field.label_tag }}
    {{ field }}
    {% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}
    {{ field.errors }}
  </div>
{% endfor %}

<!-- Non-field errors -->
{{ form.non_field_errors }}

Crispy forms / form rendering

uv add django-crispy-forms crispy-bootstrap5
{% load crispy_forms_tags %}
{{ form|crispy }}

Formsets (multiple of the same form)

from django.forms import formset_factory

ItemFormSet = formset_factory(ItemForm, extra=3, max_num=10, can_delete=True)

def view(request):
    if request.method == "POST":
        fs = ItemFormSet(request.POST)
        if fs.is_valid():
            for form in fs:
                if form.cleaned_data and not form.cleaned_data.get("DELETE"):
                    form.save()
    else:
        fs = ItemFormSet()

Inline formset

from django.forms import inlineformset_factory

CommentFormSet = inlineformset_factory(Post, Comment, fields=["body"], extra=2)

def edit_post(request, id):
    post = get_object_or_404(Post, id=id)
    if request.method == "POST":
        fs = CommentFormSet(request.POST, instance=post)
        if fs.is_valid():
            fs.save()
    else:
        fs = CommentFormSet(instance=post)

Custom field

class CommaSeparatedField(forms.CharField):
    def to_python(self, value):
        if not value: return []
        return [v.strip() for v in value.split(",") if v.strip()]

File upload

class UploadForm(forms.Form):
    file = forms.FileField()

def view(request):
    if request.method == "POST":
        form = UploadForm(request.POST, request.FILES)
        if form.is_valid():
            f = form.cleaned_data["file"]
            # f.name, f.size, f.content_type
            with open(f"./uploads/{f.name}", "wb") as out:
                for chunk in f.chunks():
                    out.write(chunk)

Dynamic forms

class DynamicForm(forms.Form):
    def __init__(self, *args, user=None, **kwargs):
        super().__init__(*args, **kwargs)
        if user and user.is_staff:
            self.fields["admin_only"] = forms.BooleanField()

HTMX + forms

<form hx-post="{% url 'blog:save' %}" hx-swap="outerHTML">
  {% csrf_token %}
  {{ form }}
</form>
def save(request):
    form = MyForm(request.POST)
    if form.is_valid():
        form.save()
        return render(request, "blog/_form.html", {"form": MyForm(), "saved": True})
    return render(request, "blog/_form.html", {"form": form})

Common mistakes

  • Forgetting {% csrf_token %} — POST fails.
  • ModelForm without fields or exclude — error.
  • commit=False then no form.save_m2m() — M2M lost.
  • Validation in view instead of clean() — fragmented.
  • widget=...(attrs={"class": "..."}, required=True)required goes on field, not widget.

Read this next

If you want my form rendering snippets, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .