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

bash
pip install django

Output: (none — exits 0 on success)

Create a project and app

bash
django-admin startproject mysite
cd mysite
python manage.py startapp blog

Output:

text
(no output — creates directory structure)

Generated structure:

text
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.

python
# 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.

bash
# Add 'blog' to INSTALLED_APPS in mysite/settings.py first
python manage.py makemigrations
python manage.py migrate

Output:

text
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

python
# 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"]
bash
python manage.py createsuperuser
python manage.py runserver

Output:

text
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 runserver in production — it is single-threaded and not hardened. Use gunicorn + nginx or a managed platform (Railway, Fly.io, Heroku).

DEBUG = True in production — Django shows full tracebacks with local variable values in the browser when DEBUG=True. Set DEBUG=False and ALLOWED_HOSTS correctly 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 (from django-extensions) instead of manage.py shell — it auto-imports all models.

Richer example — class-based view with JSON response

python
# 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})
python
# mysite/urls.py
from django.urls import path
from blog.views import PostListView

urlpatterns = [
    path("api/posts/", PostListView.as_view()),
]
bash
curl -s http://127.0.0.1:8000/api/posts/

Output:

text
{"posts": [{"id": 1, "title": "Hello Django", "created_at": "2026-04-25T12:00:00Z"}]}

Essential management commands

CommandPurpose
manage.py runserverStart dev server on 127.0.0.1:8000
manage.py makemigrationsGenerate migration files from model changes
manage.py migrateApply pending migrations to the database
manage.py createsuperuserCreate an admin user
manage.py shellInteractive Python shell with Django loaded
manage.py dbshellSQL shell for the configured database
manage.py collectstaticCopy static files to STATIC_ROOT for serving
manage.py testRun the test suite
manage.py checkValidate 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.

python
# 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"}
python
# 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.

python
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")
python
# 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).

python
# 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
python
# 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.

python
# 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)
python
# 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.

python
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:

text
alice: 42 articles
bob: 27 articles
carol: 19 articles

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.

python
# 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

bash
# Create a migration for model changes
python manage.py makemigrations myapp

Output:

text
Migrations for 'myapp':
  myapp/migrations/0003_article_slug.py
    - Add field slug to article
bash
# Empty migration for custom data migration
python manage.py makemigrations myapp --empty --name=backfill_slugs

Output:

text
Migrations for 'myapp':
  myapp/migrations/0004_backfill_slugs.py
python
# 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)]
bash
# Show migration plan
python manage.py showmigrations myapp

Output:

text
myapp
 [X] 0001_initial
 [X] 0002_add_status
 [X] 0003_article_slug
 [ ] 0004_backfill_slugs
bash
python manage.py migrate myapp

Output:

text
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.

python
# 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"),
    }
}