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
fieldsorexclude— error. commit=Falsethen noform.save_m2m()— M2M lost.- Validation in view instead of
clean()— fragmented. widget=...(attrs={"class": "..."}, required=True)—requiredgoes 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 .