Skip to content

Django 치트시트

High-level Python web framework for rapid, secure development.

01

Getting Started

Install & Create Project

Install Django via pip. A project holds settings and URLs; an app is a reusable module with models and views.

django
pip install django
django-admin startproject myproject
cd myproject
python manage.py startapp myapp

Run Development Server

Starts the built-in dev server with auto-reload. Never use in production.

django
python manage.py runserver
# Default: http://127.0.0.1:8000
python manage.py runserver 8080
python manage.py runserver 0.0.0.0:8000

Migrations & Superuser

makemigrations generates migration files from model changes. migrate applies them to the DB. createsuperuser creates an admin account.

django
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser

Project Structure

manage.py is the CLI tool. The inner package holds project config; the app directory holds app-level code.

django
myproject/
  manage.py            # CLI utility
  myproject/
    settings.py        # Configuration
    urls.py            # Root URL dispatcher
    wsgi.py            # WSGI entry point
    asgi.py            # ASGI entry point
  myapp/
    models.py          # Database models
    views.py           # View functions
    admin.py           # Admin registration

Settings Basics

INSTALLED_APPS lists active apps. Set DEBUG=False and configure ALLOWED_HOSTS in production.

django
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'myapp',
]
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}
DEBUG = True
ALLOWED_HOSTS = []
02

Model Fields

Define a Model

Each model class maps to a DB table. Each field maps to a column. __str__ defines the display name.

django
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    published = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

Common Field Types

CharField requires max_length. auto_now_add sets on creation; auto_now updates on every save. EmailField/URLField add validation.

django
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.IntegerField(default=0)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
email = models.EmailField(unique=True)
url = models.URLField(blank=True)

Field Options

null is DB-level; blank is validation-level. choices creates a dropdown in forms and admin.

django
title = models.CharField(
    max_length=200,
    null=True,          # DB allows NULL
    blank=True,         # Form allows empty
    default='',
    choices=[('draft', 'Draft'), ('pub', 'Published')],
    help_text='Article title',
    verbose_name='Article Title',
)

Meta Options

ordering sets default sort order. db_table overrides the auto-generated table name. constraints enforce DB-level rules.

django
class Meta:
    ordering = ['-created_at']
    db_table = 'articles'
    verbose_name_plural = 'Articles'
    constraints = [
        models.UniqueConstraint(
            fields=['title', 'author'],
            name='unique_title_per_author',
        )
    ]

Model Methods & Properties

Override save() for pre-save logic. @property adds computed attributes. Always call super().save().

django
class Article(models.Model):
    title = models.CharField(max_length=200)
    views = models.IntegerField(default=0)

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        self.title = self.title.strip()
        super().save(*args, **kwargs)

    @property
    def is_popular(self):
        return self.views > 1000
03

Model Relationships

ForeignKey (Many-to-One)

ForeignKey creates a many-to-one relationship. related_name enables reverse queries: author.books.all().

django
class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    author = models.ForeignKey(
        Author,
        on_delete=models.CASCADE,
        related_name='books',
    )
    title = models.CharField(max_length=200)

OneToOneField

OneToOneField creates a one-to-one link. Access via user.profile or profile.user.

django
class Profile(models.Model):
    user = models.OneToOneField(
        User,
        on_delete=models.CASCADE,
    )
    bio = models.TextField()

ManyToManyField

ManyToManyField creates a junction table automatically. Add/remove: course.students.add(student).

django
class Student(models.Model):
    name = models.CharField(max_length=100)

class Course(models.Model):
    students = models.ManyToManyField(Student, blank=True)
    title = models.CharField(max_length=200)

on_delete Options

CASCADE deletes children when parent is deleted. SET_NULL requires null=True. PROTECT raises ProtectedError.

django
# CASCADE - delete related objects
author = models.ForeignKey(Author, on_delete=models.CASCADE)

# SET_NULL - set to NULL (field must be nullable)
author = models.ForeignKey(Author, on_delete=models.SET_NULL, null=True)

# PROTECT - prevent deletion
author = models.ForeignKey(Author, on_delete=models.PROTECT)

# SET_DEFAULT - set to default value
author = models.ForeignKey(Author, on_delete=models.SET_DEFAULT, default=1)

Related Objects Query

Forward access uses the field name. Reverse access uses related_name (or modelname_set by default).

django
# Forward query
book.author.name

# Reverse query (uses related_name)
author.books.all()
author.books.filter(title__contains='Django')

# Add/remove in M2M
course.students.add(student1, student2)
course.students.remove(student1)
course.students.clear()

# Check membership
student in course.students.all()
04

ORM QuerySet

Create & Save Objects

create() saves immediately. get_or_create returns (object, created_bool) — atomically gets or creates.

django
# Method 1: Create and save separately
article = Article(title='Hello', body='World')
article.save()

# Method 2: Create in one step
article = Article.objects.create(title='Hello', body='World')

# Method 3: get_or_create
obj, created = Article.objects.get_or_create(
    title='Hello', defaults={'body': 'World'}
)

Retrieve Single Object

get() raises DoesNotExist if not found, MultipleObjectsReturned if multiple. Use first() to safely get one.

django
# Get by primary key
article = Article.objects.get(pk=1)

# Get by field
article = Article.objects.get(title='Hello')

# First/last (returns None if empty)
first = Article.objects.first()
last = Article.objects.last()

# Latest/earliest by field
latest = Article.objects.latest('created_at')

Filter & Exclude

filter() returns a QuerySet (lazy). Chaining applies AND logic. QuerySets are evaluated only when iterated.

django
# Filter
Article.objects.filter(title__contains='Django')
Article.objects.filter(published=True)

# Exclude
Article.objects.exclude(views__lt=100)

# Chain filters (AND)
Article.objects.filter(author=1).exclude(draft=True)

Field Lookups

Lookups use double-underscore syntax. Common: exact, contains, icontains, gt, gte, lt, lte, in, startswith, range.

django
Article.objects.filter(title__icontains='django')  # case-insensitive
Article.objects.filter(views__gte=100)              # >=
Article.objects.filter(views__in=[100, 200, 300])   # IN
Article.objects.filter(created_at__year=2024)       # date lookup
Article.objects.filter(title__startswith='Hello')   # LIKE 'Hello%'
Article.objects.filter(title__isnull=True)          # IS NULL

Ordering & Slicing

Slicing applies LIMIT at the DB level. Negative indexing is not supported on QuerySets.

django
# Order by
Article.objects.order_by('-created_at')  # descending
Article.objects.order_by('author', '-views')

# Slice (LIMIT/OFFSET)
Article.objects.all()[:5]       # first 5
Article.objects.all()[5:10]     # offset 5, limit 5
Article.objects.order_by('-views')[:3]  # top 3

# Distinct
Article.objects.values('author').distinct()

Update & Delete

update() is efficient for bulk changes. save(update_fields=[...]) avoids race conditions. delete() cascades by default.

django
# Bulk update
Article.objects.filter(draft=True).update(draft=False)

# Single update (only save this field)
article.title = 'New Title'
article.save(update_fields=['title'])

# Delete
article.delete()  # returns (count, {model: count})
Article.objects.filter(views=0).delete()  # bulk delete
05

ORM Advanced

F Expressions

F() references a DB column value, enabling atomic updates without loading the object into Python.

django
from django.db.models import F

# Atomic increment (no race condition)
Article.objects.filter(pk=1).update(views=F('views') + 1)

# Compare two fields
Article.objects.filter(views__gt=F('min_views'))

# Arithmetic on fields
Article.objects.annotate(ratio=F('views') / F('likes'))

Q Objects for Complex Queries

Q objects enable OR (|), AND (&), and NOT (~) logic. Without Q, multiple filter() calls are AND-only.

django
from django.db.models import Q

# OR
Article.objects.filter(Q(title__contains='Django') | Q(title__contains='Flask'))

# AND with NOT
Article.objects.filter(Q(published=True) & ~Q(views=0))

# OR + AND
Article.objects.filter(
    (Q(author=1) | Q(author=2)) & Q(published=True)
)

Aggregation

aggregate() returns a dict of totals. annotate() adds per-object computed values.

django
from django.db.models import Count, Sum, Avg, Max, Min

# Single aggregate
total = Article.objects.aggregate(total=Count('id'))
avg_views = Article.objects.aggregate(Avg('views'))

# Per-group aggregate
Author.objects.annotate(
    article_count=Count('article'),
    total_views=Sum('article__views'),
)

Annotate

annotate() adds computed fields per object. You can filter() and order_by() on annotated fields.

django
from django.db.models import Count, Avg

# Add computed field to each object
authors = Author.objects.annotate(article_count=Count('book'))

# Filter on annotated field
Author.objects.annotate(
    avg_rating=Avg('book__rating')
).filter(avg_rating__gte=4.0)

select_related & prefetch_related

select_related uses SQL JOIN (forward FK). prefetch_related uses a second query (reverse/M2M). Both prevent N+1 queries.

django
# select_related (FK/OneToOne - single query with JOIN)
books = Book.objects.select_related('author').all()
# book.author.name  -> no extra query

# prefetch_related (M2M/reverse - second query)
authors = Author.objects.prefetch_related('books').all()
# author.books.all()  -> no extra query

# Deep prefetch
Author.objects.prefetch_related('books', 'books__reviews').all()
06

Function-Based Views

Basic Function View

A view takes an HttpRequest and returns an HttpResponse. render() loads a template and fills it with context.

django
from django.shortcuts import render
from django.http import HttpResponse

def hello(request):
    return HttpResponse("Hello, World!")

def home(request):
    context = {'title': 'Home', 'items': [1, 2, 3]}
    return render(request, 'home.html', context)

HttpRequest Object

request.GET/POST are QueryDict (use .get() for safety). request.user is the authenticated user or AnonymousUser.

django
def my_view(request):
    method = request.method           # 'GET' or 'POST'
    get_data = request.GET.get('q')   # query param
    post_data = request.POST.get('name')
    path = request.path               # '/articles/1/'
    user = request.user               # current user
    files = request.FILES             # uploaded files
    meta = request.META.get('HTTP_HOST')

HttpResponse Types

JsonResponse serializes to JSON. redirect() takes a URL path or a named URL with args.

django
from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect

def json_view(request):
    return JsonResponse({'status': 'ok', 'count': 42})

def redirect_view(request):
    return redirect('/home/')
    return redirect('article_detail', pk=1)

def csv_view(request):
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = 'attachment; filename="data.csv"'
    return response

View Decorators

login_required redirects unauthenticated users to LOGIN_URL. require_POST/GET restrict HTTP methods.

django
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST, require_GET

@login_required
def dashboard(request):
    return render(request, 'dashboard.html')

@require_POST
def submit(request):
    pass  # Only accepts POST

@require_GET
def search(request):
    pass  # Only accepts GET

File Upload View

request.FILES contains uploaded files. Use .chunks() for large files to avoid loading entire file into memory.

django
def upload_file(request):
    if request.method == 'POST':
        uploaded = request.FILES['file']
        Document.objects.create(file=uploaded)
        # Or save manually
        with open(f'media/{uploaded.name}', 'wb') as f:
            for chunk in uploaded.chunks():
                f.write(chunk)
        return redirect('success')
    return render(request, 'upload.html')
07

Class-Based Views

TemplateView

TemplateView renders a static template. Override get_context_data() to add extra context.

django
from django.views.generic import TemplateView

class AboutView(TemplateView):
    template_name = 'about.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['title'] = 'About Us'
        return context

ListView

ListView auto-paginates and provides object_list and page_obj in context. Override get_queryset() for filtering.

django
from django.views.generic import ListView

class ArticleListView(ListView):
    model = Article
    template_name = 'article_list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        return Article.objects.filter(published=True)

DetailView

DetailView fetches a single object by pk or slug from the URL. Override get_object() for custom fetching.

django
from django.views.generic import DetailView

class ArticleDetailView(DetailView):
    model = Article
    template_name = 'article_detail.html'
    context_object_name = 'article'

    def get_object(self):
        return Article.objects.get(
            slug=self.kwargs['slug'], published=True
        )

CreateView & UpdateView

CreateView/UpdateView handle form display, validation, and saving. Override form_valid() to add logic before save.

django
from django.views.generic.edit import CreateView, UpdateView

class ArticleCreateView(CreateView):
    model = Article
    fields = ['title', 'body', 'author']
    template_name = 'article_form.html'
    success_url = '/articles/'

class ArticleUpdateView(UpdateView):
    model = Article
    fields = ['title', 'body']
    template_name = 'article_form.html'

Mixins

Mixins add reusable behavior to class-based views. Django provides LoginRequiredMixin, PermissionRequiredMixin.

django
from django.contrib.auth.mixins import LoginRequiredMixin

class OwnerRequiredMixin:
    def get_queryset(self):
        return super().get_queryset().filter(owner=self.request.user)

class ArticleListView(LoginRequiredMixin, OwnerRequiredMixin, ListView):
    model = Article
    paginate_by = 10
08

URL Routing

Basic URL Configuration

path() maps a URL pattern to a view. name= enables reverse URL lookup. <int:pk> captures an integer.

django
from django.urls import path
from myapp import views

urlpatterns = [
    path('', views.home, name='home'),
    path('about/', views.about, name='about'),
    path('article/<int:pk>/', views.article_detail, name='article_detail'),
]

Including App URLs

include() delegates URL matching to an app's urls.py. app_name creates a namespace for URL reversing.

django
# myproject/urls.py
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
    path('api/', include('api.urls')),
]

# blog/urls.py
app_name = 'blog'
urlpatterns = [
    path('', views.list, name='list'),
    path('<int:pk>/', views.detail, name='detail'),
]

Path Converters

Path converters auto-type-cast URL segments. Use re_path for complex regex patterns.

django
path('article/<int:pk>/')        # integer
path('article/<slug:slug>/')     # slug string
path('article/<str:title>/')     # non-slash string
path('user/<uuid:user_id>/')    # UUID
path('file/<path:filepath>/')   # any string including /

# Regex path (re_path)
from django.urls import re_path
re_path(r'^article/(?P<year>[0-9]{4})/$', views.year_archive)

Named URLs & reverse()

Named URLs let you reference routes by name instead of hardcoding paths. reverse() generates URLs in Python.

django
# urls.py
path('article/<int:pk>/', views.detail, name='article_detail')

# In views.py
from django.urls import reverse
url = reverse('article_detail', kwargs={'pk': 42})
# Returns '/article/42/'

# In templates
<a href="{% url 'article_detail' pk=article.pk %}">Read</a>

URL Namespaces

Namespaces prevent name collisions between apps. The namespace must match the app's app_name.

django
# myproject/urls.py
path('blog/', include(('blog.urls', 'blog'), namespace='blog'))

# blog/urls.py
app_name = 'blog'
urlpatterns = [
    path('<int:pk>/', views.detail, name='detail'),
]

# Usage
reverse('blog:detail', kwargs={'pk': 1})
# Template: {% url 'blog:detail' pk=1 %}
09

Templates (DTL)

Template Inheritance

extends inherits from a base template. Blocks define overridable regions. A template can extend only one parent.

django
{# base.html #}
<html>
<body>
  {% block content %}{% endblock %}
  {% block sidebar %}{% endblock %}
</body>
</html>

{# article.html #}
{% extends 'base.html' %}
{% block content %}
  <h1>{{ article.title }}</h1>
{% endblock %}

Variables & Tags

{{ }} outputs variables. {% %} executes tags. forloop.counter starts at 1; forloop.counter0 at 0.

django
{# Variables #}
{{ article.title }}
{{ user.get_full_name }}
{{ article.tags.0 }}

{# Tags #}
{% if article.published %}
    Published
{% elif article.draft %}
    Draft
{% else %}
    Archived
{% endif %}

{% for item in items %}
    {{ forloop.counter }}: {{ item }}
{% empty %}
    No items
{% endfor %}

Template Filters

Filters modify variable output using pipe | syntax. Some take arguments after a colon. Filters can be chained.

django
{{ name|lower }}
{{ text|truncatewords:30 }}
{{ date|date:"Y-m-d" }}
{{ value|default:"N/A" }}
{{ list|length }}
{{ text|striptags }}
{{ price|floatformat:2 }}
{{ value|add:5 }}

Built-in Template Tags

csrf_token is required in all POST forms. include renders a sub-template. load imports custom tag libraries.

django
{% url 'article_detail' pk=1 %}
{% csrf_token %}
{% load static %}
{% include 'header.html' %}
{% with total=items|length %}
    {{ total }}
{% endwith %}
{% now "Y-m-d" %}
{% blocktrans %}Hello{% endblocktrans %}

Custom Template Tags

Tags live in app/templatetags/ directory. @register.simple_tag for functions; @register.filter for value modifiers.

django
# templatetags/my_tags.py
from django import template
register = template.Library()

@register.simple_tag
def multiply(a, b):
    return a * b

@register.filter
def cut(value, arg):
    return value.replace(arg, '')

# In template:
{% load my_tags %}
{% multiply 3 4 %}
{{ "hello world"|cut:" " }}
10

Forms

Define a Form

forms.Form creates a standalone form. Each field auto-generates HTML widgets. Field types provide validation.

django
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea)
    subject = forms.ChoiceField(
        choices=[('general', 'General'), ('bug', 'Bug Report')],
    )

ModelForm

ModelForm auto-builds a form from a model. fields/exclude control which fields appear. widgets customize HTML.

django
class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ['title', 'body', 'author']
        # Or: fields = '__all__'
        # Or: exclude = ['created_at']
        widgets = {
            'body': forms.Textarea(attrs={'rows': 10}),
        }
        labels = {'title': 'Article Title'}

Form Validation

clean_<field>() validates a single field. clean() validates the entire form. Raise ValidationError on failure.

django
class SignupForm(forms.Form):
    username = forms.CharField(max_length=50)
    password = forms.CharField(widget=forms.PasswordInput)

    def clean_username(self):
        username = self.cleaned_data['username']
        if User.objects.filter(username=username).exists():
            raise forms.ValidationError("Username taken.")
        return username

    def clean(self):
        cleaned = super().clean()
        # Cross-field validation
        return cleaned

Render Form in Template

as_p/as_table/as_ul auto-render all fields. For custom layout, render fields individually. csrf_token is required for POST.

django
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    {{ form.as_table }}
    {{ form.as_ul }}

    {# Manual rendering #}
    {{ form.title.label_tag }}
    {{ form.title }}
    {{ form.title.errors }}

    <button type="submit">Submit</button>
</form>

Process Form in View

Pass request.POST to bind the form. is_valid() runs validation. save(commit=False) returns an unsaved object.

django
def article_create(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user
            article.save()
            form.save_m2m()
            return redirect('article_detail', pk=article.pk)
    else:
        form = ArticleForm()
    return render(request, 'form.html', {'form': form})

Form Widgets

widgets control HTML rendering. Pass attrs dict for CSS classes, placeholders, or HTML5 input types.

django
class UploadForm(forms.Form):
    title = forms.CharField(
        widget=forms.TextInput(attrs={'class': 'form-control'})
    )
    file = forms.FileField(widget=forms.ClearableFileInput)
    date = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'}))
    tags = forms.MultipleChoiceField(
        choices=TAG_CHOICES,
        widget=forms.CheckboxSelectMultiple,
    )
11

Admin Site

Register Model in Admin

@admin.register is preferred over admin.site.register. Models must be registered to appear in the admin interface.

django
from django.contrib import admin
from .models import Article

# Simple registration
admin.site.register(Article)

# With custom ModelAdmin
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ('title', 'author', 'published')

ModelAdmin Customization

list_display shows columns in list view. list_filter adds sidebar filters. search_fields enables a search box.

django
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ('title', 'author', 'status')
    list_filter = ('status', 'author', 'created_at')
    search_fields = ('title', 'body')
    ordering = ('-created_at',)
    list_editable = ('status',)
    list_per_page = 25
    date_hierarchy = 'created_at'
    readonly_fields = ('views', 'created_at')

Inline Admin

Inlines edit related models on the same page as the parent. extra=1 shows one empty row by default.

django
class ReviewInline(admin.TabularInline):
    model = Review
    extra = 1

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    inlines = [ReviewInline]

# TabularInline: compact table format
# StackedInline: full form fields stacked

Custom Admin Actions

Actions appear in the dropdown on the list page. They receive request and queryset. message_user shows a flash message.

django
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    actions = ['publish_selected']

    @admin.action(description='Publish selected')
    def publish_selected(self, request, queryset):
        updated = queryset.update(published=True)
        self.message_user(request, f'{updated} articles published.')

Admin Site Configuration

Override site_header/site_title for branding. For multiple admin sites, create custom AdminSite instances.

django
admin.site.site_header = 'My Site Admin'
admin.site.site_title = 'Admin Portal'
admin.site.index_title = 'Dashboard'

# Custom admin site
from django.contrib.admin import AdminSite
my_site = AdminSite(name='myadmin')
my_site.register(Article, ArticleAdmin)

# urls.py
path('myadmin/', my_site.urls)
12

Authentication

Built-in User Model

Django's User model has username, password (hashed), email, first_name, last_name, and permission flags.

django
from django.contrib.auth.models import User

user = User.objects.get(username='admin')
user.is_staff        # can access admin
user.is_superuser    # has all permissions
user.is_active       # account enabled
user.set_password('newpass')  # hash password
user.save()
user.check_password('secret')  # returns True/False

Login & Logout

authenticate() verifies credentials (returns User or None). login() creates the session. logout() clears it.

django
from django.contrib.auth import authenticate, login, logout

def login_view(request):
    user = authenticate(request, username='admin', password='pass')
    if user is not None:
        login(request, user)
        return redirect('dashboard')
    return render(request, 'login.html')

def logout_view(request):
    logout(request)
    return redirect('home')

User Registration

UserCreationForm handles username, password, and password confirmation with built-in validation.

django
from django.contrib.auth.forms import UserCreationForm

def register(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)  # auto-login after register
            return redirect('home')
    else:
        form = UserCreationForm()
    return render(request, 'register.html', {'form': form})

Login Required Decorator

login_required redirects to LOGIN_URL (default /accounts/login/). For CBVs, use LoginRequiredMixin.

django
from django.contrib.auth.decorators import login_required

@login_required
def profile(request):
    return render(request, 'profile.html')

@login_required(login_url='/custom-login/')
def settings(request):
    return render(request, 'settings.html')

# Class-based view mixin
from django.contrib.auth.mixins import LoginRequiredMixin
class DashboardView(LoginRequiredMixin, ListView):
    login_url = '/login/'
    model = Article

Password Management

update_session_auth_hash prevents logout after password change. Password validators enforce rules site-wide.

django
from django.contrib.auth import update_session_auth_hash

# Change password
user.set_password('newpass123')
user.save()
update_session_auth_hash(request, user)  # keep user logged in

# Using form
from django.contrib.auth.forms import PasswordChangeForm
form = PasswordChangeForm(user, request.POST)
if form.is_valid():
    form.save()

Custom User Model

Subclass AbstractUser to add fields. Set AUTH_USER_MODEL early — changing it later is difficult.

django
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    bio = models.TextField(blank=True)
    avatar = models.ImageField(upload_to='avatars/', blank=True)
    phone = models.CharField(max_length=20, blank=True)

# settings.py
AUTH_USER_MODEL = 'myapp.CustomUser'

# Admin
from django.contrib.auth.admin import UserAdmin
@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
    pass
13

Middleware

Built-in Middleware

Order matters! SecurityMiddleware first, then sessions, then auth. CSRF checks run before view processing.

django
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

Custom Function Middleware

Function middleware uses the new-style factory pattern. get_response calls the next middleware or view.

django
def simple_middleware(get_response):
    def middleware(request):
        # Code before view (request phase)
        response = get_response(request)
        # Code after view (response phase)
        return response
    return middleware

# Example: timing middleware
import time
def timing_middleware(get_response):
    def middleware(request):
        start = time.time()
        response = get_response(request)
        response['X-Duration'] = str(time.time() - start)
        return response
    return middleware

Class-Based Middleware

__init__ runs once at startup. __call__ runs per request. process_view can return a response to bypass the view.

django
class MyMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # Request phase (before view)
        response = self.get_response(request)
        # Response phase (after view)
        return response

    def process_view(self, request, view_func, args, kwargs):
        return None  # or HttpResponse to short-circuit

Middleware Order & Flow

Requests pass through middleware top-to-bottom; responses bubble up bottom-to-top. Put security middleware first.

django
# Request phase: top to bottom
# Response phase: bottom to top

MIDDLEWARE = [
    'A',  # req: 1st -> resp: last
    'B',  # req: 2nd -> resp: 2nd
    'C',  # req: 3rd -> resp: 1st
]

# Request flows: A -> B -> C -> view
# Response flows: view -> C -> B -> A

Middleware Use Cases

Middleware can short-circuit (return HttpResponse early) or attach data to request for use in views.

django
# IP restriction
class IPBlockMiddleware:
    BLOCKED_IPS = ['192.168.1.100']
    def __call__(self, request):
        if request.META['REMOTE_ADDR'] in self.BLOCKED_IPS:
            return HttpResponseForbidden('Blocked')
        return self.get_response(request)

# Add context to request
class UserPrefMiddleware:
    def __call__(self, request):
        request.is_mobile = 'Mobile' in request.META.get('HTTP_USER_AGENT', '')
        return self.get_response(request)
14

Static & Media Files

Static Files Configuration

STATIC_URL is the URL prefix. STATICFILES_DIRS are source locations (dev). STATIC_ROOT is the production collection target.

django
# settings.py
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']  # dev location
STATIC_ROOT = BASE_DIR / 'staticfiles'     # collectstatic output

# Project structure:
# static/
#   css/style.css
#   js/app.js
#   img/logo.png

Serving Static in Development

Django auto-serves static files only when DEBUG=True. In production, use a web server (nginx) or WhiteNoise.

django
# urls.py (dev only)
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

# Only active when DEBUG=True

Using Static in Templates

{% load static %} is required. {% static %} resolves to the hashed filename in production with ManifestStaticFilesStorage.

django
{% load static %}

<link rel="stylesheet" href="{% static 'css/style.css' %}">
<img src="{% static 'img/logo.png' %}" alt="Logo">
<script src="{% static 'js/app.js' %}"></script>

{# Dynamic static path #}
{% get_static_prefix as STATIC_URL %}
<img src="{{ STATIC_URL }}img/{{ theme }}.png">

Media Files Configuration

MEDIA_ROOT is the filesystem path; MEDIA_URL is the URL prefix. upload_to can be a path string or a callable.

django
# settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

# urls.py (dev)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

# Model with file upload
class Document(models.Model):
    file = models.FileField(upload_to='documents/')
    image = models.ImageField(upload_to='images/')

# Access
doc.file.url    # '/media/documents/file.pdf'
doc.file.path   # '/full/path/to/file.pdf'

collectstatic Command

Run collectstatic before deployment. WhiteNoise lets Django serve static files in production without nginx.

django
# Collect all static files into STATIC_ROOT
python manage.py collectstatic

# With clearing
python manage.py collectstatic --clear

# Production with WhiteNoise
# pip install whitenoise
MIDDLEWARE = [..., 'whitenoise.middleware.WhiteNoiseMiddleware', ...]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
15

Sessions & Cookies

Session Configuration

Default session engine stores in DB. cached_db writes to cache + DB. signed_cookies stores in client cookies.

django
# settings.py
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
SESSION_COOKIE_AGE = 86400  # seconds (1 day)
SESSION_SAVE_EVERY_REQUEST = False
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_COOKIE_SECURE = True  # HTTPS only (production)
SESSION_COOKIE_HTTPONLY = True  # JS cannot access

# Engines: db, cached_db, cache, file, signed_cookies

Set & Get Session Data

Session acts like a dict. .get() avoids KeyError. flush() clears data and creates a new session key (for logout).

django
def set_session(request):
    request.session['user_id'] = 42
    request.session['theme'] = 'dark'
    request.session['cart'] = [1, 2, 3]

def get_session(request):
    user_id = request.session.get('user_id')       # None if missing
    user_id = request.session.get('user_id', 0)    # with default

    # Delete
    del request.session['theme']
    request.session.flush()  # clear all + regenerate key

Session Backends

cache is fastest but volatile. cached_db is a good balance. signed_cookies has a 4KB limit but no server storage.

django
# Database (default)
SESSION_ENGINE = 'django.contrib.sessions.backends.db'

# Cache (fastest, lost on restart)
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'

# Cache + DB (fast reads, persistent)
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'

# File-based
SESSION_ENGINE = 'django.contrib.sessions.backends.file'

# Signed cookies (no server storage, 4KB limit)
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'

Cookies

set_cookie adds to response. request.COOKIES reads incoming cookies. httponly prevents JS access (XSS protection).

django
# Set a cookie
response = HttpResponse('Hello')
response.set_cookie('key', 'value', max_age=3600)
response.set_cookie(
    'pref', 'dark',
    max_age=86400, httponly=True,
    secure=True, samesite='Lax',
)

# Read a cookie
value = request.COOKIES.get('key')

# Delete a cookie
response.delete_cookie('key')

Session Expiry

set_expiry(0) = browser close. set_expiry(None) = use settings. Run clearsessions as a cron job to clean up.

django
# Set expiry for this session
request.session.set_expiry(1800)  # 30 minutes
request.session.set_expiry(0)     # expire on browser close
request.session.set_expiry(None)  # use SESSION_COOKIE_AGE

# Check expiry
request.session.get_expiry_age()
request.session.get_expire_at_browser_close()

# Clear expired sessions (cron job)
python manage.py clearsessions
16

Signals

Built-in Signals

Signals allow decoupled apps to react to actions. post_save fires after save() completes; pre_save fires before.

django
from django.db.models.signals import (
    post_save, pre_save, post_delete, pre_delete,
    m2m_changed,
)
from django.contrib.auth.signals import (
    user_logged_in, user_logged_out,
)

# Common signals:
# pre_save / post_save      - before/after model save
# pre_delete / post_delete   - before/after model delete
# m2m_changed               - M2M relationship changes
# user_logged_in / user_logged_out

Connecting Signal Receivers

@receiver connects the function to the signal. sender limits to a specific model. created is True only on new objects.

django
from django.db.models.signals import post_save
from django.dispatch import receiver

@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()

Disconnecting Signals

disconnect() is useful in tests or when you need to prevent cascading actions. Always provide both receiver and sender.

django
from django.db.models.signals import post_save

# Disconnect
post_save.disconnect(
    receiver=create_profile,
    sender=User,
)

# Temporarily disconnect in tests
post_save.disconnect(handler, sender=Article)
# ... do stuff without signal ...
post_save.connect(handler, sender=Article)

Custom Signals

Define signals at module level. send() notifies all receivers. kwargs are passed to all connected receivers.

django
from django.dispatch import Signal, receiver

# Define a custom signal
order_created = Signal()

# Send the signal
order_created.send(
    sender=Order,
    order=order_instance,
    user=request.user,
)

# Receive
@receiver(order_created, sender=Order)
def send_notification(sender, order, user, **kwargs):
    pass  # Send email notification

Signal Use Cases & Gotchas

Import signal handlers in AppConfig.ready() to ensure they connect at startup. Avoid slow operations — use Celery for async work.

django
# Good: Profile auto-creation
@receiver(post_save, sender=User)
def ensure_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.get_or_create(user=instance)

# Register in AppConfig.ready()
# apps.py
class MyAppConfig(AppConfig):
    name = 'myapp'
    def ready(self):
        from . import signals  # noqa
17

Caching

Cache Configuration

Redis is recommended for production. LocMemCache is process-local (no sharing between workers).

django
# settings.py - Memcached
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
        'LOCATION': '127.0.0.1:11211',
    }
}

# Redis
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379',
    }
}

# Local memory (dev)
CACHES = {
    'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}
}

Low-Level Cache API

cache.get() returns None for missing keys. Use get_or_set for set-if-not-exists atomicity.

django
from django.core.cache import cache

# Set
cache.set('key', 'value', timeout=300)  # 5 minutes
cache.set_many({'a': 1, 'b': 2}, timeout=60)

# Get
value = cache.get('key')              # None if missing
value = cache.get('key', 'default')   # with default
data = cache.get_many(['a', 'b'])     # dict

# Delete
cache.delete('key')
cache.clear()  # delete all

# get_or_set (atomic)
value = cache.get_or_set('key', 'value', 300)

Template Caching

{% cache %} stores rendered HTML fragments. Add varying arguments (user ID, page) to avoid wrong cached content.

django
{% load cache %}

{# Cache block for 500 seconds #}
{% cache 500 sidebar %}
    <div class="sidebar">
        {{ expensive_data }}
    </div>
{% endcache %}

{# Cache with varying key #}
{% cache 500 user_sidebar request.user.id %}
    Welcome, {{ request.user.username }}
{% endcache %}

Per-View Cache

cache_page caches the entire HttpResponse. The cache key includes the URL, so each URL gets its own cache entry.

django
from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def article_list(request):
    return render(request, 'list.html', {'articles': Article.objects.all()})

# With cache key prefix
@cache_page(60 * 15, key_prefix='articles')
def article_detail(request, pk):
    ...

# Class-based view
from django.utils.decorators import method_decorator
class ArticleListView(ListView):
    @method_decorator(cache_page(60 * 15))
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)

Cache Strategies

vary_on_headers creates separate cache entries per header value. For APIs, vary on Accept-Language or Authorization.

django
# Per-view cache with Vary headers
from django.views.decorators.vary import vary_on_headers

@cache_page(60 * 15)
@vary_on_headers('User-Agent')
def api_data(request):
    ...

# Model-level caching
def get_articles():
    articles = cache.get('all_articles')
    if articles is None:
        articles = list(Article.objects.all())
        cache.set('all_articles', articles, 300)
    return articles
18

Testing

Writing Tests

TestCase wraps tests in a DB transaction (rolled back). SimpleTestCase is for tests without DB access (faster).

django
from django.test import TestCase, SimpleTestCase
from .models import Article

class ArticleModelTest(TestCase):
    def setUp(self):
        self.article = Article.objects.create(
            title='Test', body='Content'
        )

    def test_str(self):
        self.assertEqual(str(self.article), 'Test')

    def test_field_default(self):
        self.assertEqual(self.article.views, 0)

# SimpleTestCase: no DB access
class MathTest(SimpleTestCase):
    def test_addition(self):
        self.assertEqual(1 + 1, 2)

Testing with Test Client

self.client simulates HTTP requests without starting a server. .login() authenticates for subsequent requests.

django
class ViewTest(TestCase):
    def setUp(self):
        self.user = User.objects.create_user('test', '[email protected]', 'pass')

    def test_login(self):
        response = self.client.post('/login/', {
            'username': 'test', 'password': 'pass'
        })
        self.assertEqual(response.status_code, 302)

    def test_protected_view(self):
        self.client.login(username='test', password='pass')
        response = self.client.get('/dashboard/')
        self.assertEqual(response.status_code, 200)

Model & QuerySet Tests

Test data is created in setUp() and rolled back after each test. Use .count() and indexing to verify results.

django
class ArticleQuerySetTest(TestCase):
    def setUp(self):
        Article.objects.create(title='A', views=100, published=True)
        Article.objects.create(title='B', views=50, published=False)

    def test_published_filter(self):
        published = Article.objects.filter(published=True)
        self.assertEqual(published.count(), 1)

    def test_ordering(self):
        articles = Article.objects.order_by('-views')
        self.assertEqual(articles[0].title, 'A')

URL & Form Tests

reverse() builds URLs from names. resolve() checks which view a URL maps to. Test forms by passing data= dict.

django
from django.urls import reverse, resolve

class URLTest(TestCase):
    def test_article_list_url(self):
        url = reverse('article_list')
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

    def test_resolve(self):
        url = reverse('article_detail', kwargs={'pk': 1})
        self.assertEqual(resolve(url).func, views.article_detail)

class FormTest(TestCase):
    def test_valid_form(self):
        form = ArticleForm(data={'title': 'Hi', 'body': '...'})
        self.assertTrue(form.is_valid())

Fixtures & Test Commands

Fixtures load test data from JSON files. Run specific tests by app, class, or method path.

django
# Create fixture
python manage.py dumpdata myapp.Article --indent=2 > fixtures/articles.json

# Use in test
class FixtureTest(TestCase):
    fixtures = ['articles.json']

    def test_data_loaded(self):
        self.assertEqual(Article.objects.count(), 10)

# Run tests
python manage.py test
python manage.py test myapp
python manage.py test myapp.tests.ArticleModelTest
19

Django REST Framework

Install & Setup DRF

Add rest_framework to INSTALLED_APPS. Configure default auth/permission in REST_FRAMEWORK settings dict.

django
pip install djangorestframework

# settings.py
INSTALLED_APPS = [..., 'rest_framework', 'rest_framework.authtoken']

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

Serializers

ModelSerializer auto-generates fields from the model. source= pulls from related objects. validate_<field>() adds validation.

django
from rest_framework import serializers

class ArticleSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source='author.username', read_only=True)

    class Meta:
        model = Article
        fields = ['id', 'title', 'body', 'author', 'author_name']
        read_only_fields = ['id', 'author_name']

    def validate_title(self, value):
        if len(value) < 5:
            raise serializers.ValidationError("Title too short.")
        return value

ViewSets

ModelViewSet provides CRUD (list, create, retrieve, update, destroy). Override get_queryset() for filtering.

django
from rest_framework import viewsets, permissions

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def get_queryset(self):
        qs = super().get_queryset()
        author = self.request.query_params.get('author')
        if author:
            qs = qs.filter(author__username=author)
        return qs

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

Routers

DefaultRouter auto-generates URL patterns and an API root view. basename is used for URL naming if queryset is dynamic.

django
from rest_framework.routers import DefaultRouter
from .views import ArticleViewSet

router = DefaultRouter()
router.register(r'articles', ArticleViewSet, basename='article')
# router.register(r'users', UserViewSet)

# urls.py
urlpatterns = [
    path('api/', include(router.urls)),
]
# Generates: /api/articles/, /api/articles/{id}/

Authentication

Token auth uses a single static token. JWT issues access + refresh tokens. SimpleJWT is the most popular JWT library.

django
# Token Authentication
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
    path('api/token/', obtain_auth_token),
]

# JWT Authentication (pip install djangorestframework-simplejwt)
from rest_framework_simplejwt.views import (
    TokenObtainPairView, TokenRefreshView,
)
urlpatterns = [
    path('api/token/', TokenObtainPairView.as_view()),
    path('api/token/refresh/', TokenRefreshView.as_view()),
]

Permissions

SAFE_METHODS are GET/HEAD/OPTIONS. has_object_permission checks per-object access. get_permissions() allows per-action rules.

django
from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True  # GET, HEAD, OPTIONS
        return obj.author == request.user

# Built-in: IsAuthenticated, IsAdminUser,
# IsAuthenticatedOrReadOnly, AllowAny

# Per-action
class ArticleViewSet(viewsets.ModelViewSet):
    permission_classes = [IsOwnerOrReadOnly]
    def get_permissions(self):
        if self.action == 'create':
            return [permissions.IsAuthenticated()]
        return super().get_permissions()
20

Deployment & Security

Production Settings

Never commit SECRET_KEY to version control. Use environment variables. DEBUG=False prevents info leaks in error pages.

django
# settings.py
DEBUG = False
ALLOWED_HOSTS = ['example.com', 'www.example.com']
SECRET_KEY = os.environ['SECRET_KEY']

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mydb',
        'USER': os.environ['DB_USER'],
        'PASSWORD': os.environ['DB_PASS'],
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

Static Files in Production

collectstatic gathers all static files to STATIC_ROOT. WhiteNoise serves gzipped, hashed files from Django itself.

django
# Collect static files
python manage.py collectstatic --noinput

# WhiteNoise for serving static
MIDDLEWARE = [..., 'whitenoise.middleware.WhiteNoiseMiddleware', ...]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

# Or with nginx:
# location /static/ { alias /path/to/staticfiles/; }

Security Settings

SSL_REDIRECT forces HTTPS. HSTS tells browsers to always use HTTPS. X_FRAME_OPTIONS prevents clickjacking.

django
# settings.py
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
CSRF_TRUSTED_ORIGINS = ['https://example.com']

WSGI/ASGI Deployment

Gunicorn is synchronous (traditional Django). Uvicorn supports ASGI (async views, websockets). Use a process manager.

django
# Gunicorn (WSGI)
# pip install gunicorn
gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 3

# Uvicorn (ASGI, for async/websockets)
# pip install uvicorn
uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000 --workers 4

# Supervisor config
[program:django]
command=gunicorn myproject.wsgi:application
directory=/app
autostart=true
autorestart=true

Production Checklist

check --deploy runs Django's deployment checklist. It flags missing security settings and production-readiness issues.

django
# Run security check
python manage.py check --deploy

# Key items:
# - DEBUG = False
# - ALLOWED_HOSTS set
# - SECRET_KEY from env
# - HTTPS enabled (SECURE_SSL_REDIRECT)
# - HSTS configured
# - collectstatic run
# - Database (PostgreSQL/MySQL)
# - Caching (Redis/Memcached)
# - Error monitoring (Sentry)

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.