Start hereQuick start · 6 you’ll reach for daily
Target versions · paceVersions
django ≥ 5.1
python ≥ 3.10
DRF ≥ 3.15 (optional)
Django 5.x is the LTS family. Async views, async ORM (.aget,
.acount, async signals) are stable.
DEFAULT_AUTO_FIELD defaults to BigAutoField.
Names current as of May 2026.
install · scaffold · manageSetup
# Install + scaffold
pip install "django>=5.1"
django-admin startproject mysite .
python manage.py startapp blog
# Daily-loop commands
python manage.py runserver 0.0.0.0:8000
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
python manage.py shell # IPython shell with project loaded
# Static + collect
python manage.py collectstatic --noinput
python manage.py check # system check, never skip
# Optional but ubiquitous
pip install djangorestframework # REST APIs
pip install django-environ # env-driven settings
pip install django-debug-toolbar # dev
where things liveCommon imports
| from django.db import models, transaction, connection | ORM + transactional helpers. |
| from django.urls import path, re_path, include, reverse, reverse_lazy | Routing + reverse URL building. |
| from django.shortcuts import render, redirect, get_object_or_404, get_list_or_404 | View-level shortcuts. |
| from django.http import HttpResponse, JsonResponse, Http404, HttpResponseRedirect | Low-level responses. |
| from django.views.generic import View, ListView, DetailView, CreateView, UpdateView, DeleteView, FormView | CBV stdlib. |
| from django.contrib.auth import get_user_model, login, logout, authenticate | Auth helpers. Always get_user_model(). |
| from django.contrib.auth.decorators import login_required, permission_required | FBV gates. |
| from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin | CBV gates. |
| from django import forms | Form classes. |
| from django.core.cache import cache | Cache backend. |
| from django.conf import settings | Read settings at runtime. |
| from django.contrib import admin, messages | Admin + flash messages. |
| from rest_framework import serializers, viewsets, permissions, routers | DRF core (optional). |
declarative ORMModels
| CharField(max_length=…) | Strings. Always set max_length. |
| TextField() | Unbounded text. No length limit. |
| SlugField, EmailField, URLField, UUIDField, JSONField | Validated specialised text / json. |
| IntegerField, BigIntegerField, DecimalField(max_digits, decimal_places) | Numbers. Money → Decimal. |
| DateField, DateTimeField(auto_now_add=True), TimeField, DurationField | Temporal. |
| BooleanField(default=False) | Always default explicitly; NULL booleans are confusing. |
| ForeignKey(Other, on_delete=models.CASCADE, related_name="…") | M:1. Pick on_delete deliberately. |
| ManyToManyField(Other, through="X", related_name="…") | M:N. through for extra fields. |
| OneToOneField(Other, on_delete=…) | Subclassing-style relations. |
| choices = TextChoices / IntegerChoices | Typed enum. Better than tuples. |
| Meta: ordering, indexes, constraints, unique_together | Schema-level metadata. |
| UniqueConstraint(fields=[…], name=…) | Replaces legacy unique_together. |
| CheckConstraint(check=Q(qty__gte=0), name=…) | DB-level validation. |
| Index(fields=["status", "-created"]) | Composite + descending indexes. |
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Post(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "Draft"
PUBLISHED = "published", "Published"
title = models.CharField(max_length=200, db_index=True)
slug = models.SlugField(max_length=220, unique=True)
body = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts")
tags = models.ManyToManyField("Tag", related_name="posts", blank=True)
status = models.CharField(max_length=10, choices=Status.choices, default=Status.DRAFT)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created"]
indexes = [models.Index(fields=["status", "-created"])]
constraints = [
models.UniqueConstraint(fields=["author", "slug"], name="uniq_author_slug"),
]
def __str__(self) -> str:
return self.title
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
schema changesMigrations
| manage.py makemigrations | Detect model changes → new file. |
| manage.py makemigrations app --name add_indexes | Custom name. Helps history. |
| manage.py migrate | Apply pending migrations. |
| manage.py migrate app 0003 | Migrate forward / backward to a specific point. |
| manage.py sqlmigrate app 0003 | Show generated SQL without running. |
| manage.py showmigrations | List + applied flags. |
| manage.py makemigrations --empty app | Hand-written data migration. |
| RunPython(forward, reverse) | In-Python data fix-ups inside a migration. |
| manage.py squashmigrations app 0001 0050 | Collapse history once stable. |
QuerySet APIORM
| Post.objects.all() / .filter(status=…) / .exclude(…) | Lazy QuerySets. Compose freely. |
| .get(id=1) / .first() / .last() | Single-row reads. get raises if not exactly one. |
| __exact, __icontains, __gt, __gte, __in, __isnull, __range | Lookup suffixes. |
| __date / __year / __week_day | Date-component lookups. |
| Q(a=1) | Q(b=2) / ~Q(c=3) | Boolean composition. |
| F("qty") + 1 | Reference DB columns inside expressions. |
| .annotate(n=Count("tags"), total=Sum("price")) | Compute per-row aggregates. |
| .aggregate(Avg("score")) | Single-row aggregate over the whole set. |
| .values("id", "name") / .values_list(…, flat=True) | Project to dicts / tuples. |
| .select_related("author") | Preferred JOIN for FK / O2O. Cuts N+1. |
| .prefetch_related("tags") | Separate query for M2M / reverse FK. Cuts N+1. |
| .only("id", "title") / .defer("body") | Limit columns loaded. |
| .update(status="archived") | SQL-level update. Skips save(). |
| .bulk_create(objs, batch_size=500) | One INSERT for many rows. |
| .iterator(chunk_size=1000) | Stream a big QuerySet without caching. |
| await Post.objects.aget(id=1) | Async variants exist for most methods. |
FBV & CBVViews
| def view(request): return render(request, "t.html", ctx) | Function-based view (FBV). |
| class PostList(ListView): model = Post | Class-based view (CBV). |
| ListView, DetailView, CreateView, UpdateView, DeleteView, FormView | Stock CBVs covering 80% of CRUD. |
| paginate_by = 20 | Pagination built into ListView. |
| get_queryset / get_context_data | Override hook points. |
| @login_required / @require_http_methods(["POST"]) | FBV decorators. |
| LoginRequiredMixin, PermissionRequiredMixin | CBV mixins. Put first. |
| async def view(request) / @sync_to_async | Async views. ORM has async too (aget, …). |
| return JsonResponse({"ok": True}) | JSON response. safe=False for non-dict roots. |
| raise Http404("reason") | Equivalent to get_object_or_404. |
routingURLs
| path("posts/<int:id>/", view, name="detail") | Preferred Path converters. |
| <str:>, <int:>, <slug:>, <uuid:>, <path:> | Built-in converters. |
| re_path(r"^posts/(?P<slug>[-\w]+)/$", view) | Regex fallback for complex patterns. |
| app_name = "blog" | Namespace. Reference as blog:detail. |
| include("blog.urls", namespace="blog") | Mount under a prefix. |
| reverse("blog:detail", kwargs={"id":1}) | Build URLs from names. |
| {% url "blog:detail" pk=p.pk %} | Same in templates. |
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.PostList.as_view(), name="list"),
path("posts//", views.PostDetail.as_view(), name="detail"),
path("posts/new/", views.PostCreate.as_view(), name="create"),
]
# mysite/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls", namespace="blog")),
]
# blog/views.py
from django.shortcuts import get_object_or_404, render, redirect
from django.contrib.auth.decorators import login_required
from django.views.generic import ListView, DetailView, CreateView
from django.urls import reverse_lazy
from .models import Post
class PostList(ListView):
model = Post
paginate_by = 10
queryset = Post.objects.filter(status=Post.Status.PUBLISHED)
class PostDetail(DetailView):
model = Post
class PostCreate(CreateView):
model = Post
fields = ["title", "slug", "body"]
success_url = reverse_lazy("blog:list")
Django Template LanguageTemplates
| {{ value }} | Variable interpolation (autoescaped). |
| {{ value|date:"Y-m-d" }} | Filters. |
| {% for x in items %} … {% endfor %} | Loops + forloop.counter / .last. |
| {% if cond %} … {% elif %} … {% endif %} | Conditional. |
| {% extends "base.html" %} / {% block content %} | Inheritance. |
| {% include "partial.html" with x=1 %} | Inline another template. |
| {% static "css/app.css" %} | From {% load static %}. |
| {% url "blog:detail" pk=1 %} | Reverse a URL by name. |
| {% csrf_token %} | Required inside HTML forms (POST). |
| {{ value|safe }} | Bypass autoescape. Trusted strings only. |
validation + renderForms
| class MyForm(forms.Form): name = forms.CharField() | Plain form. |
| class MyForm(forms.ModelForm): class Meta: model, fields | Tied to a model. |
| form.is_valid() / form.cleaned_data | Validate + access typed data. |
| form.errors / form.non_field_errors() | Per-field / form-wide errors. |
| def clean_email(self): … | Per-field validation. Raise ValidationError. |
| def clean(self): … | Cross-field validation. |
| widgets = {"body": forms.Textarea(…)} | Override widgets. |
| form.save(commit=False) | Get an unsaved model instance. |
batteries-included CRUDAdmin
| admin.site.register(Post) | Quickest. Default ModelAdmin. |
| @admin.register(Post) | Decorator form, paired with a class. |
| list_display = ("title", "status", "created") | Columns in the changelist. |
| list_filter / search_fields / date_hierarchy / ordering | Standard knobs. |
| prepopulated_fields = {"slug": ("title",)} | Auto-fill from another field. |
| readonly_fields / inlines | Lock + nested editing. |
| def get_queryset(self, request): … | Scope admin per-user. |
| @admin.action(description="Publish") | Bulk actions on selected rows. |
users, sessions, permsAuth & permissions
| get_user_model() | Preferred Avoid importing User directly. |
| authenticate(request, username=…, password=…) | Returns user or None. |
| login(request, user) / logout(request) | Session-based. |
| @login_required(login_url="/login") | FBV gate. |
| @permission_required("blog.change_post") | Permission codename gate. |
| user.has_perm("blog.change_post") | Programmatic check. |
| AUTH_USER_MODEL = "accounts.User" | Set before first migration to avoid pain. |
| AbstractBaseUser / AbstractUser | Custom user base classes. |
| PermissionsMixin | Adds groups + per-object perms support. |
configurationSettings & deployment
| DEBUG = False (in prod) | Never True with public traffic. |
| ALLOWED_HOSTS = ["example.com"] | Required when DEBUG is off. |
| SECRET_KEY = env("SECRET_KEY") | Load from env / secrets manager. |
| DATABASES["default"] = env.db() | Connection from DATABASE_URL. |
| CACHES["default"] = redis backend | Add Redis for sessions / fragments. |
| SECURE_HSTS_SECONDS, SECURE_SSL_REDIRECT, CSRF_TRUSTED_ORIGINS | Set behind HTTPS / proxy. |
| STATIC_ROOT + collectstatic | Run before deploy; serve via nginx / whitenoise. |
| gunicorn site.wsgi:application --workers 3 | WSGI server. |
| uvicorn site.asgi:application --workers 3 | ASGI for async views / WebSockets. |
| manage.py check --deploy | Production-readiness audit. |
REST APIsDjango REST framework
| serializers.ModelSerializer | Auto serialiser from a model. |
| viewsets.ModelViewSet | CRUD endpoints in one class. |
| permission_classes = [IsAuthenticatedOrReadOnly] | Per-view auth policy. |
| DefaultRouter().register("posts", ViewSet) | Auto URL generation. |
| filter_backends = [SearchFilter, OrderingFilter] | Plug-in query backends. |
| pagination_class = PageNumberPagination | Page / cursor / limit-offset paginators. |
| throttle_classes = [UserRateThrottle] | Rate limits. |
| @action(detail=True, methods=["post"]) | Custom endpoint on a ViewSet. |
# api/serializers.py
from rest_framework import serializers
from blog.models import Post
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ["id", "title", "slug", "body", "status", "created"]
read_only_fields = ["id", "created"]
# api/views.py
from rest_framework import viewsets, permissions, filters
from .serializers import PostSerializer
from blog.models import Post
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all().order_by("-created")
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
search_fields = ["title", "body"]
ordering_fields = ["created", "title"]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
# api/urls.py
from rest_framework.routers import DefaultRouter
from .views import PostViewSet
router = DefaultRouter()
router.register("posts", PostViewSet)
urlpatterns = router.urls
model · CBV · URLEnd-to-end · Notes app
Smallest viable Django app: model, two CBVs, URLconf. Hook into INSTALLED_APPS,
migrate, runserver, done.
# End-to-end: model -> migrate -> URL -> class-based view -> template -> serve.
# blog/models.py
from django.db import models
class Note(models.Model):
body = models.CharField(max_length=280)
created = models.DateTimeField(auto_now_add=True)
class Meta: ordering = ["-created"]
# blog/views.py
from django.views.generic import ListView, CreateView
from django.urls import reverse_lazy
from .models import Note
class NoteList(ListView):
model = Note; paginate_by = 20
class NoteCreate(CreateView):
model = Note; fields = ["body"]; success_url = reverse_lazy("blog:list")
# blog/urls.py
from django.urls import path
from .views import NoteList, NoteCreate
app_name = "blog"
urlpatterns = [
path("", NoteList.as_view(), name="list"),
path("new/", NoteCreate.as_view(), name="create"),
]
# Then:
# python manage.py makemigrations blog
# python manage.py migrate
# python manage.py runserver
Best practiceGood to know
select_related / prefetch_related early.
The N+1 query is the most common Django performance bug. Add them as soon as you iterate a queryset and
touch a relation.
AUTH_USER_MODEL = "accounts.User" before the first migration costs
nothing. Adding it later is a multi-day surgery.
manage.py check --deploy in CI.
Catches missing security headers, debug-on-in-prod, weak SECRET_KEY — the
classic deploy-day surprises.
Common trapsWatch out for
RunPython data migration in between.
.update() bypasses save() and signals.
Custom save() logic, auto_now,
post_save signals — none of these fire. Use loop + save()
when you need them.
User directly.
from django.contrib.auth.models import User hard-codes the default and
breaks when someone swaps in a custom user. Always get_user_model().