cheat sheet
django
Build database-driven web applications with Django. Covers project setup, models, migrations, the admin panel, views, forms, class-based views, middleware, signals, ORM advanced patterns, and the management command workflow.
django — Full-Stack Web Framework
What it is
Django is a batteries-included web framework: ORM, migrations, admin panel, authentication, form handling, and template engine all come built-in. It follows the MTV (Model–Template–View) pattern and enforces a project layout that scales from a simple blog to a large multi-app system.
Install
pip install django
Output: (none — exits 0 on success)
Create a project and app
django-admin startproject mysite
cd mysite
python manage.py startapp blog
Output:
(no output — creates directory structure)
Generated structure:
mysite/
├── manage.py
├── mysite/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── blog/
├── models.py
├── views.py
├── admin.py
└── migrations/
Define a model
A Django model is a Python class that maps to a database table; each class attribute is a field with a type (CharField, TextField, ForeignKey, etc.) and optional constraints. Django reads these definitions to generate SQL CREATE TABLE statements and to provide the ORM query API. The optional db_table meta attribute overrides the default table name if needed.
# blog/models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str:
return self.title
Run migrations
Django's migration system is a two-step process: makemigrations compares your current models against the last migration state and writes a new Python migration file, then migrate executes those files against the database in order. Always commit migration files alongside the model changes that generated them.
# Add 'blog' to INSTALLED_APPS in mysite/settings.py first
python manage.py makemigrations
python manage.py migrate
Output:
Migrations for 'blog':
blog/migrations/0001_initial.py
- Create model Post
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
Applying blog.0001_initial... OK
Register in admin and create superuser
# blog/admin.py
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "published", "created_at"]
list_filter = ["published"]
search_fields = ["title", "body"]
python manage.py createsuperuser
python manage.py runserver
Output:
Username: admin
Email address: admin@example.com
Password: ••••••••
Superuser created successfully.
Watching for file changes with StatReloader
Django version 5.1.0, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Browse to http://127.0.0.1:8000/admin/ to log in and manage Post records.
When / why to use it
- Full web applications that need user auth, admin panel, and a built-in ORM.
- Content-heavy sites (CMS, blogs, e-commerce).
- Teams that want strong conventions and a large ecosystem of third-party apps (
django-rest-framework,django-allauth, etc.).
Prefer Flask for small APIs with no admin requirements. Prefer FastAPI for async, high-performance APIs.
Common pitfalls
Never run
manage.py runserverin production — it is single-threaded and not hardened. Usegunicorn+ nginx or a managed platform (Railway, Fly.io, Heroku).
DEBUG = Truein production — Django shows full tracebacks with local variable values in the browser whenDEBUG=True. SetDEBUG=FalseandALLOWED_HOSTScorrectly in production, and use environment variables for secret settings.
Migration conflicts in teams — when two developers add different migrations, you get a conflict. Resolve with:
python manage.py makemigrations --merge.
Use
python manage.py shell_plus(fromdjango-extensions) instead ofmanage.py shell— it auto-imports all models.
Richer example — class-based view with JSON response
# blog/views.py
from django.http import JsonResponse
from django.views import View
from .models import Post
class PostListView(View):
def get(self, request):
posts = list(
Post.objects.filter(published=True)
.values("id", "title", "created_at")
.order_by("-created_at")[:10]
)
return JsonResponse({"posts": posts})
# mysite/urls.py
from django.urls import path
from blog.views import PostListView
urlpatterns = [
path("api/posts/", PostListView.as_view()),
]
curl -s http://127.0.0.1:8000/api/posts/
Output:
{"posts": [{"id": 1, "title": "Hello Django", "created_at": "2026-04-25T12:00:00Z"}]}
Essential management commands
| Command | Purpose |
|---|---|
manage.py runserver | Start dev server on 127.0.0.1:8000 |
manage.py makemigrations | Generate migration files from model changes |
manage.py migrate | Apply pending migrations to the database |
manage.py createsuperuser | Create an admin user |
manage.py shell | Interactive Python shell with Django loaded |
manage.py dbshell | SQL shell for the configured database |
manage.py collectstatic | Copy static files to STATIC_ROOT for serving |
manage.py test | Run the test suite |
manage.py check | Validate project configuration |
Forms and ModelForms
Form defines fields manually and is used when the form does not map directly to a model (e.g. a contact or search form). ModelForm introspects a model class and auto-generates matching fields, making it the right choice when you want to create or update a model instance from user input. Both provide is_valid() and cleaned_data after POST.
# forms.py
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea, min_length=10)
subscribe = forms.BooleanField(required=False)
class ContactModelForm(forms.ModelForm):
class Meta:
model = Contact
fields = ["name", "email", "message"]
widgets = {"message": forms.Textarea(attrs={"rows": 4})}
labels = {"message": "Your message"}
# views.py — form handling
from django.shortcuts import render, redirect
def contact_view(request):
if request.method == "POST":
form = ContactForm(request.POST)
if form.is_valid():
# form.cleaned_data is a dict of validated values
name = form.cleaned_data["name"]
send_email(name, form.cleaned_data["email"], form.cleaned_data["message"])
return redirect("contact-success")
else:
form = ContactForm()
return render(request, "contact.html", {"form": form})
Class-Based Views
CBVs replace function-based views with classes that inherit from generic views (ListView, DetailView, CreateView, etc.), eliminating repetitive GET/POST branching. The trade-off is that they are easier to extend via get_queryset() / get_context_data() overrides but harder to read at a glance than a straightforward FBV. Use CBVs for standard CRUD; use FBVs when the logic is irregular enough to make inheritance awkward.
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
class ArticleListView(ListView):
model = Article
template_name = "articles/list.html"
context_object_name = "articles"
paginate_by = 20
ordering = ["-created_at"]
def get_queryset(self):
qs = super().get_queryset()
if q := self.request.GET.get("q"):
qs = qs.filter(title__icontains=q)
return qs
class ArticleDetailView(DetailView):
model = Article
template_name = "articles/detail.html"
class ArticleCreateView(CreateView):
model = Article
fields = ["title", "body", "tags"]
success_url = reverse_lazy("article-list")
class ArticleUpdateView(UpdateView):
model = Article
fields = ["title", "body"]
success_url = reverse_lazy("article-list")
class ArticleDeleteView(DeleteView):
model = Article
success_url = reverse_lazy("article-list")
# urls.py
from django.urls import path
from .views import ArticleListView, ArticleDetailView, ArticleCreateView
urlpatterns = [
path("articles/", ArticleListView.as_view(), name="article-list"),
path("articles/<int:pk>/", ArticleDetailView.as_view(), name="article-detail"),
path("articles/new/", ArticleCreateView.as_view(), name="article-create"),
]
Middleware
Middleware is a stack of callables that wraps every request/response cycle globally — each layer can inspect or modify the request before the view runs and the response before it is returned. Django applies the MIDDLEWARE list top-to-bottom on requests and bottom-to-top on responses, so order matters (e.g. SecurityMiddleware must come before session or auth middleware).
# myapp/middleware.py
import time
import logging
logger = logging.getLogger(__name__)
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start = time.monotonic()
response = self.get_response(request)
duration = time.monotonic() - start
logger.info("%s %s %.3fs %d", request.method, request.path, duration, response.status_code)
response["X-Response-Time"] = f"{duration:.3f}s"
return response
# settings.py — register middleware
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"myapp.middleware.TimingMiddleware", # <-- add here
"django.contrib.sessions.middleware.SessionMiddleware",
...
]
Signals
Signals let decoupled parts of an application react to model events (post_save, pre_delete, etc.) without the emitting code knowing anything about the receivers. They are useful for side-effects like creating related records or sending notifications, but overuse makes control flow hard to trace — prefer direct calls when the relationship between caller and side-effect is obvious.
# signals.py
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import Profile
@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
instance.profile.save()
@receiver(pre_delete, sender=User)
def log_user_deletion(sender, instance, **kwargs):
logger.warning("User %s (%d) is being deleted", instance.username, instance.pk)
# apps.py — connect signals
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self):
import myapp.signals # noqa: F401
ORM — Q objects and F expressions
Q objects let you build complex WHERE clauses with &, |, and ~ operators that cannot be expressed with simple .filter(a=x, b=y) keyword arguments. F expressions reference a column's value directly in the database, enabling field-to-field comparisons and in-database arithmetic without pulling data into Python — critical for race-condition-free updates like incrementing a counter.
from django.db.models import Q, F, Count, Sum, Avg
# Q objects — complex WHERE conditions
articles = Article.objects.filter(
Q(status="published") & (Q(author__username="alice") | Q(tags__name="django"))
).distinct()
# Negate with ~Q
draft_or_unpublished = Article.objects.filter(~Q(status="published"))
# F expressions — reference another field in the same row
# Give a 10% raise to all employees in Engineering
Employee.objects.filter(dept="Engineering").update(salary=F("salary") * 1.10)
# Compare two fields without pulling to Python
outdated = Product.objects.filter(price__lt=F("cost")) # selling below cost
# Annotate with aggregates
from django.db.models import Count
authors = User.objects.annotate(article_count=Count("article")).order_by("-article_count")
for a in authors[:3]:
print(f"{a.username}: {a.article_count} articles")
Output:
alice: 42 articles
bob: 27 articles
carol: 19 articles
select_related and prefetch_related
Both methods eliminate the N+1 query problem, but they work differently: select_related issues a single SQL JOIN and is appropriate for ForeignKey and OneToOne relations. prefetch_related fires a separate query per relation and assembles results in Python — use it for ManyToMany fields or reverse FK relations where a JOIN would produce too many duplicate rows.
# N+1 problem (BAD — fires one query per article)
for article in Article.objects.all():
print(article.author.username) # separate SQL per row
# select_related — SQL JOIN for ForeignKey / OneToOne (one query total)
for article in Article.objects.select_related("author").all():
print(article.author.username) # no extra query
# prefetch_related — separate query for ManyToMany / reverse FK
for article in Article.objects.prefetch_related("tags").all():
print([tag.name for tag in article.tags.all()]) # no extra query
# Combine both
articles = Article.objects.select_related("author").prefetch_related("tags", "comments")
Migrations workflow
# Create a migration for model changes
python manage.py makemigrations myapp
Output:
Migrations for 'myapp':
myapp/migrations/0003_article_slug.py
- Add field slug to article
# Empty migration for custom data migration
python manage.py makemigrations myapp --empty --name=backfill_slugs
Output:
Migrations for 'myapp':
myapp/migrations/0004_backfill_slugs.py
# 0004_backfill_slugs.py — data migration
from django.db import migrations
from django.utils.text import slugify
def backfill_slugs(apps, schema_editor):
Article = apps.get_model("myapp", "Article")
for article in Article.objects.filter(slug=""):
article.slug = slugify(article.title)
article.save(update_fields=["slug"])
def reverse_backfill(apps, schema_editor):
Article = apps.get_model("myapp", "Article")
Article.objects.update(slug="")
class Migration(migrations.Migration):
dependencies = [("myapp", "0003_article_slug")]
operations = [migrations.RunPython(backfill_slugs, reverse_backfill)]
# Show migration plan
python manage.py showmigrations myapp
Output:
myapp
[X] 0001_initial
[X] 0002_add_status
[X] 0003_article_slug
[ ] 0004_backfill_slugs
python manage.py migrate myapp
Output:
Operations to perform:
Apply all migrations: myapp
Running migrations:
Applying myapp.0004_backfill_slugs... OK
Settings and environment
SECRET_KEY must be a long random string kept out of source control — use os.environ or a library like django-environ. Set DEBUG=False and a restrictive ALLOWED_HOSTS in production; leaving DEBUG=True exposes full stack traces and local variables to anyone who triggers a 500 error. Database credentials similarly belong in environment variables, not committed settings files.
# settings.py — split into base/local/production
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True"
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "127.0.0.1").split(",")
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("DB_NAME", "myapp"),
"USER": os.environ.get("DB_USER", "postgres"),
"PASSWORD": os.environ.get("DB_PASSWORD", ""),
"HOST": os.environ.get("DB_HOST", "localhost"),
"PORT": os.environ.get("DB_PORT", "5432"),
}
}