Quick Reference · the web framework for perfectionists with deadlines

django cheat sheet

Every Django request walks the same road: in through the middleware onion, matched by a URLconf, handled by a view, which asks a model for data and hands it to a template — then back out through the same onion, in reverse. Learn that road and the framework stops being a pile of files.

URLs / routing views · forms · middleware models · ORM · migrations templates · static · admin destructive / unsafe most common

Verified 2026-08-26 against Django 6.1 (current feature release) & 5.2 LTS · cross-checked against: docs.djangoproject.com (topic guides, model/QuerySet & CBV reference, middleware, django-admin) · Django 6.0 & 5.2 LTS release notes · djangoproject.com/download (support timeline)

The request/response cycle — and where your code plugs in
Browser GET /books/7/ HTTP WSGI / ASGI gunicorn MIDDLEWARE · the onion settings.MIDDLEWARE — order matters SecurityMiddleware SessionMiddleware AuthenticationMiddleware → request.user CsrfViewMiddleware URLconf → View request response request ↓ top→bottom response ↑ bottom→top Model Book.objects .select_related() the ORM Database SQL Postgres · SQLite Template book_detail.html {{ book.title }} render(req, tpl, ctx) HTML HttpResponse status 200 ask for data render it 1 · REQUEST 2 · MIDDLEWARE → 3 · URLconf → 4 · VIEW 5 · MODEL · 6 · TEMPLATE Any middleware layer can short-circuit — return a response without calling the next layer, and the view never runs. That is exactly how CSRF rejection, redirect-to-login and caching work. Only the layers already entered see the response.
01Scaffold & Runmanage.py
02Models — Fieldsmodels.py
03Models — Relationsthe shape of your data
04Migrationsmodels → schema
05ORM — ReadQuerySets
06ORM — Field Lookupsthe double underscore
07ORM — Writecreate · update · delete
08ORM — Aggregate & Optimizewhere the wins are
09URLsurls.py
10Views — Function-Basedexplicit & readable
11Views — Class-Basedless code, more magic
12TemplatesDTL
13Formsvalidation, not HTML
14Adminfree CRUD
15Auth & Userscontrib.auth
16Settings & Staticconfig
17Testing & Datamanage.py test
18New in Django 6.0+tasks · CSP · partials
Where Does My Code Go?the MTV decision

The things that actually confuse people

Not more API — the models underneath it.

The N+1 problem — and the two methods that kill it

The highest-leverage fix in Django, and the one juniors ship without. Accessing a related object inside a loop fires one query per row. Two method calls turn 1,001 queries into 2.

✗ NAIVE for b in Book.objects.all(): print(b.author.name) 1 query for the books… SELECT * FROM book SELECT * FROM author WHERE id=1 SELECT * FROM author WHERE id=2 …once per row, forever 1 + N queries 1,000 books ⟹ 1,001 round-trips. ✓ select_related Book.objects.select_related( "author") Django writes a JOIN: SELECT * FROM book INNER JOIN author ON … 1 query Use for: ForeignKey (forward) · OneToOneField — i.e. relations pointing to ONE object ✓ prefetch_related Book.objects.prefetch_related( "tags") Two queries, stitched in Python: SELECT * FROM book SELECT * FROM tag WHERE id IN (…) 2 queries — but never N Use for: ManyToManyField · reverse FK (book.reviews) — i.e. relations pointing to MANY objects Rule of thumb: one object → select_related. Many objects → prefetch_related. Need to filter the related rows → Prefetch(). Unsure? prefetch_related works everywhere — slightly less efficient on a FK, but never wrong. Verify with assertNumQueries or Django Debug Toolbar.

Anatomy of a field lookup

The double underscore does two different jobs in one expression: it walks relationships, and it names the comparison. Django decides which by checking whether the segment is a field on the model.

Book.objects .filter( author__country __name __icontains = "india" ) MANAGER the table you start from RELATION TRAVERSAL book → author → country Django writes the JOINs. Any depth. FINAL FIELD a real column on country LOOKUP TYPE how to compare. omit it and you get __exact by default. Lookups: exact · iexact · contains · icontains · in · gt · gte · lt · lte · startswith · endswith · range · isnull · year · date · regex

QuerySets are lazy

Building a QuerySet costs nothing. It hits the database exactly once, at the moment something forces it to.

NO SQL YET — free, chainable qs = Book.objects.all() qs = qs.filter(year=2024).exclude(draft=True) qs = qs.select_related("author").order_by("-id") …still zero queries. It's just a description of a query. NOW IT RUNS — evaluation triggers for b in qs: list(qs) len(qs) · bool(qs) qs[2:9:2] qs.count() .get() · .exists() Once evaluated it caches — a second loop is free. But re-filtering returns a new, uncached QuerySet.

on_delete — what happens to the children?

Required on every ForeignKey, because Django refuses to guess. Delete the author; what becomes of their books?

CASCADE delete the books too. The default choice — and the dangerous one. PROTECT refuse. Raises ProtectedError. Safest default for real data. RESTRICT refuse — unless something else in the same delete cascades it anyway. SET_NULL book.author = NULL. Requires null=True on the field. SET_DEFAULT · SET(callable) point the books at a replacement, e.g. an "unknown author" row. DO_NOTHING Django steps back. You now own the integrity problem. CASCADE is a footgun on user-facing data. Reach for PROTECT first.

The migration loop

A migration file is source code, not a build artefact. It gets committed, reviewed and deployed like anything else — which is why the two commands are separate.

models.py you add a field (the source of truth) 0002_add_isbn.py a numbered file, on disk COMMIT THIS review the SQL sqlmigrate books 0002 (prints, runs nothing) the database schema changed django_migrations updated makemigrations generate inspect migrate apply showmigrations [X] = applied Django tracks what it has run in a real table, django_migrations. That table — not your models — is the state it compares against. Which is why --fake is dangerous: it writes a row into that table without touching the schema. The two drift apart, silently.

Project anatomy

A project is settings + a root URLconf. An app is a reusable chunk of behaviour. One project, many apps.

myproject/ ├── manage.py # your entry point ├── config/ # the PROJECT │ ├── settings.py # INSTALLED_APPS… │ ├── urls.py # root URLconf │ └── asgi.py / wsgi.py └── books/ # an APP ├── models.py # data + rules ├── views.py # request → response ├── urls.py # you create this ├── forms.py # you create this ├── admin.py ├── tests.py ├── migrations/ # commit these! └── templates/books/ # nested on purpose

Templates nest as books/templates/books/ because Django searches all apps on one flat path — the inner folder is your namespace.

Which generic view?

Every CBV descends from View. Pick by what the page does, then override get_queryset().

View TemplateView no model needed "about us" pages ListView DetailView READ — GET only CreateView UpdateView DeleteView WRITE — GET + POST Conventions they assume (and you can override): template → books/book_list.html · book_detail.html context → object_list · object (set context_object_name) success → success_url, else the model's get_absolute_url() Fighting a CBV for more than 20 minutes? Write a function. It's allowed.

Worth memorizing

null vs blanknull is the database; blank is the form. Unrelated.
QuerySets are lazyand they cache — but re-filtering makes a fresh, uncached one
select vs prefetchone related object → JOIN. many → second query.
get() raisesDoesNotExist. Use .first() or get_object_or_404()
.update() skips save()no signals, no save() override, no auto_now
migrations are codecommit them. --fake lies to Django; the schema drifts
name every URLthen reverse() / {% url %}. Never hard-code a path
AUTH_USER_MODELset it before your first migration, or live with the pain
{% csrf_token %}in every POST form, or a 403
fat models, thin viewsa view picks data and returns a response. That's all.
check --deploybefore every release. It catches what you forgot.
DEBUG = False…and a real ALLOWED_HOSTS. Non-negotiable.