Filter concepts by levelShowing all levels.

Django · Django Fundamentals

Core concepts

Concepts
22

What Django is and the MTV pattern it is built on, then the pieces every request passes through — routing, views, models, templates and forms — and the platform features (admin, auth, sessions, i18n) that ship alongside them.

The framework

What Django is, why it ships so much by default, and the pattern it organizes an app around.

What Django is

corebeginner

Django is a Python web framework: tools and conventions for turning an HTTP request into a response, backed by a database, without wiring the plumbing yourself. Install with pip, then build on top of it.

Think of it as

Django is a kitchen that already has a stove, a fridge, and a set of knives laid out in the same drawer every time. You still cook the meal — the models, views, and templates are yours — but you never have to build the kitchen first. A bare Python web server, by contrast, is an empty room: functional, but you assemble routing, sessions, and a database layer from scratch before writing a single feature.

bash
pip install django
django-admin startproject mysite
cd mysite
python manage.py runserver

What we're doing: Scaffold a new Django project and see the pieces it generates before any app-specific code is written.

terminalbash
pip install django
django-admin startproject mysite
cd mysite
python manage.py runserver
2
startproject generates a runnable project: manage.py plus a mysite/ package holding settings.py, urls.py, asgi.py, wsgi.py.
4
runserver starts a development server — no separate web server or database setup needed to see it working.

Why this works: Every Django project starts from the same generated skeleton, which is the point: settings, URL routing, and the WSGI/ASGI entry points are already wired together and agree with each other, so the first thing you run is a working (if empty) site rather than a pile of decisions to make before anything responds to a request.

Treating Django as just a template engine

Wrong

text
"I'll use Django only to render HTML, and hand-write
the database access and routing myself."

Better

text
Use Django's URL routing, ORM, and template engine
together — they're built to interlock, not to be used
one at a time alongside hand-rolled alternatives.

What you see: Reimplementing URL dispatch or a query layer from scratch, then fighting Django's own conventions when the two don't agree.

Why: Django's pieces assume the others are present — the admin site assumes the ORM, the ORM assumes migrations, sessions assume middleware is installed. Using only the template layer throws away the batteries-included benefit that is Django's main reason to reach for it over a smaller framework.

What Django sits between

HTTP request

from a browser or client

Django

routes, queries, renders

Database

via the ORM

HTTP response

HTML, JSON, a redirect

  1. HTTP request — from a browser or client
  2. Django — routes, queries, renders
  3. Database — via the ORM
  4. HTTP response — HTML, JSON, a redirect

Remember: Django is a full Python web framework — routing, ORM, admin, auth, and templates all included and wired together, not a library you assemble piece by piece.

See also: mtv architecture · batteries included · project vs application

Batteries-included framework philosophy

standardbeginner

"Batteries-included" means Django ships an ORM, admin site, auth, sessions, and templating in the box, designed to work together — instead of picking and gluing together separate libraries yourself.

Think of it as

A microframework hands you an empty toolbox and lets you choose every tool. Django hands you a fully-stocked toolbox where every tool was chosen to fit the others — the ORM's models are what the admin site introspects, and the auth system's User model is what the session and admin both already understand. Less choice up front, but nothing to reconcile later.

python
INSTALLED_APPS = [
    "django.contrib.admin",       # admin site
    "django.contrib.auth",        # authentication
    "django.contrib.sessions",    # sessions
    "django.contrib.messages",    # flash messages
    "django.contrib.staticfiles", # static file handling
]

What we're doing: See how many of Django's "batteries" are already turned on in a freshly generated settings.py.

settings.pypython
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]
2
The admin site — a full CRUD interface generated from your models, with no view code written for it.
3
Authentication — a User model, login/logout, and permission checks, ready before any app-specific code exists.
4
Sessions — per-visitor server-side storage, addressed later in this subsection.
5
Messages — the one-time flash-notification framework, also covered later.

Why this works: A brand-new Django project already has working admin, auth, sessions, and messaging before a single line of app-specific code is written — that upfront list is what "batteries-included" means concretely, not just as a slogan. A microframework would need a separate package installed and wired up for each of these.

Assuming batteries-included means "no choices left to make"

Wrong

text
"Django decided everything for me, so I don't need
to think about which pieces my app actually uses."

Better

text
Remove INSTALLED_APPS entries and MIDDLEWARE you don't
use (e.g. sessions on a pure JSON API) — batteries-included
means available by default, not mandatory.

What you see: Unused middleware runs on every request, and unused apps still create database tables via migrations.

Why: Every entry in INSTALLED_APPS and MIDDLEWARE has a real cost — a migration, a query, a bit of per-request overhead — even if your app never uses it. Batteries-included is a starting default engineered to fit typical sites, not a fixed bundle every project must keep.

Remember: Batteries-included = ORM, admin, auth, sessions, and templates all ship together and are designed to interlock — you can still remove what you don't use.

See also: what django is · mtv architecture

MTV architecture

corebeginner

Django splits an app into Models (data and rules), Templates (how output looks), and Views (logic connecting a request to a model and a template) — its own name for a pattern close to MVC.

Think of it as

Think of a restaurant. The Model is the pantry and recipe book — what ingredients exist and the rules for combining them. The Template is the plated presentation — how the finished dish looks on its way to the table. The View is the cook: it reads the request (the order), pulls from the Model (the pantry), and hands the result to the Template (the plating) — it contains no rules about what a dish IS, and no opinion about layout.

python
# Model — models.py
class Article(models.Model):
    title = models.CharField(max_length=200)

# View — views.py
def article_detail(request, pk):
    article = Article.objects.get(pk=pk)
    return render(request, "article.html", {"article": article})

What we're doing: Trace one request through all three MTV layers for a single article page.

articles/views.pypython
from django.shortcuts import render
from .models import Article

def article_detail(request, pk):
    article = Article.objects.get(pk=pk)
    return render(request, "articles/detail.html", {"article": article})
2
Article is the Model — imported here, defined in models.py, and mapped to a database table.
4
article_detail is the View — it owns no data shape of its own, only the logic connecting request to Model to Template.
5
render() combines the "articles/detail.html" Template with a context dict — the Template never queries the database itself.

Why this works: Each layer has exactly one job: the Model defines what an Article is and how it is stored, the View decides which Article to fetch and which Template to use, and the Template only knows how to lay out whatever context dict it was handed. Keeping these separate is what Django's "loose coupling" design philosophy means in practice — you can redesign detail.html without touching views.py, and vice versa.

Putting query logic inside the template

Wrong

python
def article_detail(request, pk):
    # View passes the whole queryset and lets the template pick
    return render(request, "articles/detail.html", {
        "all_articles": Article.objects.all(),
        "pk": pk,
    })
# {% for a in all_articles %}{% if a.pk == pk %}{{ a.title }}{% endif %}{% endfor %}

Better

python
def article_detail(request, pk):
    article = Article.objects.get(pk=pk)   # View does the lookup
    return render(request, "articles/detail.html", {"article": article})
# {{ article.title }}

What you see: The template becomes hard to read and slow — it filters a whole queryset in Python inside {% for %}/{% if %} tags instead of the database doing one indexed lookup.

Why: The View exists specifically to do this kind of decision-making before the Template ever sees the data. A Template that has to search or filter is doing the View's job with a much weaker tool — the Django Template Language has no equivalent to a database WHERE clause.

How a request flows through Model, View, Template
querydatacontext

HTTP request

View

reads request, calls Model

Model

data + rules

Template

renders HTML

HTTP response

  • HTTP request
    • leads to View
  • View — reads request, calls Model
    • leads to Model (query)
    • leads to Template (context)
  • Model — data + rules
    • leads to View (data)
  • Template — renders HTML
    • leads to HTTP response
  • HTTP response

MTV compared with the more familiar MVC naming

MTV compared with the more familiar MVC naming
Django (MTV)Traditional MVCResponsibility
ModelModeldata shape and business rules
TemplateViewpresentation only, no logic
ViewControllerreads the request, talks to the Model, picks a Template
(the framework itself)ControllerDjango's URL dispatcher does some of what a Controller does too

Together

python
# models.py — Model
class Article(models.Model):
    title = models.CharField(max_length=200)

# views.py — View (the "controller" role)
def article_detail(request, pk):
    article = Article.objects.get(pk=pk)          # asks the Model
    return render(request, "article.html", {"article": article})  # picks the Template

# article.html — Template
# <h1>{{ article.title }}</h1>

Remember: Model = data/rules, Template = presentation only, View = the glue — Django's View plays MVC's "Controller" role.

See also: what django is · views · templates · models

Advertisement

Project structure

How a project differs from the reusable apps inside it, and where configuration lives.

Project vs application

standardbeginner

A project is the whole site — one settings module, one root URLconf. An app is a self-contained package inside it providing one feature (blog, billing) that could be reused elsewhere.

Think of it as

A project is a house; apps are the rooms. The house has one address and one set of utilities shared by everything inside it (the project's settings), while each room (app) has its own purpose and could, with some work, be rebuilt inside a different house. `startproject` builds the house; `startapp` builds one room inside it.

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

What we're doing: Show the file layout that startproject and startapp each generate, and how an app gets wired into the project.

directory layouttext
mysite/                # the PROJECT root
    manage.py
    mysite/
        settings.py     # one settings module for the whole project
        urls.py         # the root URL configuration
    blog/                # an APP — self-contained feature
        models.py
        views.py
        urls.py
        migrations/
7
blog/ is registered in mysite/settings.py's INSTALLED_APPS — that single line is what makes the project aware the app exists.

Why this works: Splitting a site into a project (settings, root routing — the "house") and one or more apps (features — the "rooms") is what lets an app like a third-party comment system be dropped into any project's INSTALLED_APPS without rewriting it, while the project itself stays a one-off, specific to this site.

Building one giant app instead of splitting by feature

Wrong

text
mysite/
    core/          # everything lives in one app:
        models.py  # User, Order, BlogPost, Product, all together

Better

text
mysite/
    users/     # one app per feature area
    orders/
    blog/
    catalog/

What you see: A single models.py or views.py that grows to thousands of lines, with unrelated features tangled together and no clear boundary to reuse or test independently.

Why: An app is meant to be a cohesive, ideally reusable unit — "blog" should work with or without "billing" present. Cramming every feature into one app throws away that boundary and makes the project harder to reason about as it grows, even though nothing forces the split technically.

Project vs. app at a glance

Project vs. app at a glance
PropertyProjectApp
Created bydjango-admin startprojectpython manage.py startapp
Holdssettings.py, root urls.py, asgi/wsgi.pymodels.py, views.py, its own urls.py
How many per siteexactly oneas many as needed
Reused across projects?no — it IS the siteyes, in principle — that's the design goal

Together

bash
django-admin startproject mysite    # the project — one per site
cd mysite
python manage.py startapp blog      # an app — one feature
python manage.py startapp billing   # another app — a different feature

Remember: A project is the whole site (one settings.py, one root urls.py); an app is one reusable feature inside it, created with startapp and listed in INSTALLED_APPS.

See also: what django is · app registry · settings

Django app registry

standardintermediate

The app registry (django.apps.apps) is Django's in-memory catalogue of every installed app and model, populated once at startup — how the admin site and migrations discover what models exist.

Think of it as

The registry is a building directory populated once when the building opens for the day — walk past it later and every listed app and model is instantly answerable ("is X installed?", "what fields does Y have?"), instead of the framework re-scanning every file in the building each time it needs to know.

python
# blog/apps.py
from django.apps import AppConfig

class BlogConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "blog"

    def ready(self):
        from . import signals  # connect receivers once the registry is ready

What we're doing: Register signal receivers safely by connecting them from AppConfig.ready(), which only runs after the registry has finished loading every model.

blog/apps.pypython
from django.apps import AppConfig

class BlogConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "blog"
    verbose_name = "Blog"

    def ready(self):
        from . import signals
3
BlogConfig customizes the app — this class is what the registry stores metadata against.
8
ready() runs after every app's models are importable, so importing signals here is safe — doing it at module import time earlier in startup risks importing a model before it exists.

Why this works: The registry loads in three explicit stages precisely so code like signal registration has a well-defined safe point to run: by the time ready() fires, every app's models module has already been imported, so signals.py can safely import and reference any model in the project without a circular-import risk.

Importing another app's models at the top of apps.py

Wrong

python
# blog/apps.py
from django.apps import AppConfig
from billing.models import Invoice   # imported too early

class BlogConfig(AppConfig):
    name = "blog"

Better

python
# blog/apps.py
from django.apps import AppConfig

class BlogConfig(AppConfig):
    name = "blog"

    def ready(self):
        from billing.models import Invoice   # safe — registry is populated

What you see: AppRegistryNotReady: Apps aren't loaded yet — raised during startup, often intermittently depending on INSTALLED_APPS order.

Why: apps.py is imported during stage one of registry population, before any app's models module is guaranteed to be importable — importing another app's models at that point can run before the registry is ready for it. Moving the import inside ready() (stage three) defers it until every model is safely available.

Common `django.apps.apps` registry lookups

Common `django.apps.apps` registry lookups
CallReturns
apps.get_app_config("blog")the AppConfig instance for the blog app
apps.get_app_configs()every installed AppConfig, as an iterable
apps.is_installed("blog")True if "blog" is in INSTALLED_APPS
apps.get_model("blog", "Post")the Post model class from the blog app
apps.readyTrue once the registry has finished populating

Together

python
from django.apps import apps

blog_config = apps.get_app_config("blog")
print(blog_config.verbose_name)

Post = apps.get_model("blog", "Post")
print(apps.is_installed("blog"))

Remember: The app registry populates once at startup in three stages; AppConfig.ready() is safe for cross-app imports or connecting signals.

See also: project vs application · signals · settings

Settings

standardbeginner

A Django settings file is a plain Python module — settings.py — with variables like DEBUG and ALLOWED_HOSTS. DJANGO_SETTINGS_MODULE picks which module loads; code reads it via `django.conf.settings`.

Think of it as

settings.py is the project's single dashboard of dials: DEBUG, the database connection, ALLOWED_HOSTS, installed apps — one Python module, plain variables, read once at startup. Because it's just Python, you can compute a setting (a list comprehension, an environment-variable lookup) the same as any other Python value, not just declare a static constant.

bash
export DJANGO_SETTINGS_MODULE=mysite.settings
python manage.py runserver
# or, one-off:
python manage.py runserver --settings=mysite.settings_dev

What we're doing: Read a setting correctly from application code, and see why importing it directly does not work.

blog/views.pypython
from django.conf import settings

def debug_banner(request):
    if settings.DEBUG:
        return HttpResponse("DEBUG MODE")
    return HttpResponse("")
1
settings is imported as an object from django.conf, not as the settings.py module itself.
4
settings.DEBUG reads the current value — Django resolves this against whichever module DJANGO_SETTINGS_MODULE points at.

Why this works: django.conf.settings is a lazily-evaluated proxy object, not the settings module — it resolves DJANGO_SETTINGS_MODULE the first time any setting is accessed, so the same application code works unchanged whether the environment points it at settings.py, settings_dev.py, or settings_prod.py.

Importing a single value out of the settings module directly

Wrong

python
from mysite.settings import DEBUG   # bypasses django.conf entirely

def view(request):
    if DEBUG:
        ...

Better

python
from django.conf import settings

def view(request):
    if settings.DEBUG:
        ...

What you see: Works at first, then silently reads the wrong value once a different settings module is selected via --settings or an overridden environment variable — the direct import is frozen to one specific file.

Why: `from django.conf.settings import DEBUG` does not even work — settings is an object, not a module (the docs call this out explicitly). Even `from mysite.settings import DEBUG` compiles, but it hard-codes exactly which settings module gets used, defeating the whole point of DJANGO_SETTINGS_MODULE being swappable per environment.

Settings a new project should look at first

Settings a new project should look at first
SettingWhat it controls
DEBUGverbose error pages when True — must be False in production
ALLOWED_HOSTSwhich Host headers Django will accept requests for
SECRET_KEYsigns sessions and CSRF tokens — must stay secret
DATABASESthe database engine, name, host, and credentials
INSTALLED_APPSwhich apps (built-in and yours) the registry loads

Together

python
from django.conf import settings

if settings.DEBUG:
    print("Running with verbose error pages — never in production")

Remember: A settings file is plain Python; DJANGO_SETTINGS_MODULE selects which loads; always read values via `from django.conf import settings`.

See also: project vs application · app registry · url configuration

Advertisement

Request routing

How a URL becomes a view call, and the middleware chain every request and response passes through.

URL configuration

corebeginner

A URLconf maps URL patterns to views. ROOT_URLCONF points at the module holding `urlpatterns`; `path()` defines one pattern, and `include()` lets an app plug its own URLconf in under a prefix.

Think of it as

A URLconf is a receptionist with a routing table, not a map of every possible room. A request arrives asking for a path; Django walks urlpatterns top to bottom and calls the view attached to the first pattern that matches — like a receptionist checking a numbered list of instructions rather than a building directory, first match wins, order matters.

python
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("articles/", include("articles.urls")),
]

What we're doing: Split a project's URLconf into a root file and a per-app file using include(), then capture a typed argument.

mysite/urls.pypython
from django.urls import include, path
from django.contrib import admin

urlpatterns = [
    path("admin/", admin.site.urls),
    path("articles/", include("articles.urls")),
]

# articles/urls.py
# from django.urls import path
# from . import views
#
# app_name = "articles"
# urlpatterns = [
#     path("<int:year>/<int:month>/", views.archive, name="archive"),
# ]
5
include("articles.urls") delegates everything under /articles/ to the articles app's own urlpatterns — the app owns its own routing.
12
<int:year> and <int:month> are captured and converted to Python ints before being passed to the view as positional arguments.

Why this works: include() lets an app be dropped into any project and immediately own a whole section of the URL space, without the root urls.py needing to list every one of the app's individual routes — the exact reusability project vs. application describes for apps in general.

Ordering a broad pattern before a more specific one

Wrong

python
urlpatterns = [
    path("articles/<slug:slug>/", views.detail),      # matches almost anything
    path("articles/latest/", views.latest),            # never reached
]

Better

python
urlpatterns = [
    path("articles/latest/", views.latest),            # specific pattern first
    path("articles/<slug:slug>/", views.detail),
]

What you see: Visiting /articles/latest/ calls detail(request, slug="latest") instead of latest() — Django never gets to the second pattern because the first already matched.

Why: Django stops at the first pattern that matches and calls that view — it does not look for the "best" or "most specific" match among all of urlpatterns. A slug converter matches the literal string "latest" just as happily as any other slug, so a general pattern placed first silently swallows a more specific one placed after it.

How a URLconf routes a request

/articles/42/

ROOT_URLCONF

top-level urlpatterns

first match wins

checked top to bottom

view(request, 42)

  • /articles/42/
    • leads to ROOT_URLCONF
  • ROOT_URLCONF — top-level urlpatterns
    • leads to first match wins
  • first match wins — checked top to bottom
    • leads to view(request, 42)
  • view(request, 42)

Path converters available to `path()`

Path converters available to `path()`
ConverterMatchesType passed to the view
strany non-empty string, no "/"str (the default if omitted)
intdigits onlyint
slugletters, numbers, hyphens, underscoresstr
uuida formatted UUIDuuid.UUID
pathany string, "/" includedstr

Together

python
from django.urls import path
from . import views

urlpatterns = [
    path("articles/<int:year>/", views.year_archive),
    path("articles/<slug:slug>/", views.detail),
]

Remember: ROOT_URLCONF points at a module with urlpatterns; path() maps one pattern to a view, include() delegates a prefix, and the first match wins.

See also: mtv architecture · views · settings

Views

corebeginner

A view is a Python function (or class) that takes an HttpRequest and returns an HttpResponse. It holds whatever logic is needed to answer the request — reading from a Model, choosing a Template — and it is what a URL pattern points at.

Think of it as

A view is a vending machine's dispenser mechanism: press a button (a request arrives), and it decides exactly what comes out (a response) — maybe fetching an item from stock (a Model), maybe just refusing with an error slot (a 404). It always dispenses something, never nothing, the same contract every view must satisfy.

python
from django.http import Http404
from django.shortcuts import render
from .models import Article

def detail(request, pk):
    try:
        article = Article.objects.get(pk=pk)
    except Article.DoesNotExist:
        raise Http404("Article does not exist")
    return render(request, "articles/detail.html", {"article": article})

What we're doing: Write a function-based view that looks up an object, handles the not-found case, and renders a template.

articles/views.pypython
from django.http import Http404
from django.shortcuts import render
from .models import Article

def detail(request, pk):
    try:
        article = Article.objects.get(pk=pk)
    except Article.DoesNotExist:
        raise Http404("Article does not exist")
    return render(request, "articles/detail.html", {"article": article})
5
detail(request, pk) — request is always first; pk arrives as a captured URL segment (see URL configuration).
6
The lookup can fail, so it is wrapped rather than assumed to succeed.
8
raise Http404(...) is how a view signals "not found" — Django catches it and returns a proper 404 response.
9
render() is the usual way to satisfy the "must return an HttpResponse" contract — it returns one built from a template and context.

Why this works: Every view must return an HttpResponse no matter what happens inside it — Http404 is not a way to skip that contract, it is Django's own mechanism for producing a normal (if unsuccessful) HttpResponse from an exceptional case, which is why raising it works instead of returning early with nothing.

Forgetting to return anything on one code path

Wrong

python
def detail(request, pk):
    article = Article.objects.get(pk=pk)
    if article.is_draft:
        return  # returns None — not a valid response

Better

python
from django.http import HttpResponseForbidden

def detail(request, pk):
    article = Article.objects.get(pk=pk)
    if article.is_draft:
        return HttpResponseForbidden("Not published yet")
    return render(request, "articles/detail.html", {"article": article})

What you see: ValueError: The view articles.views.detail didn't return an HttpResponse object. It returned None instead.

Why: Django checks the return value of every view and raises immediately if it is not an HttpResponse (or subclass) — there is no implicit "do nothing" response. Every branch through a view's logic, including early-exit conditions, has to end in an explicit HttpResponse.

What every view must satisfy

HttpRequest

always the first argument

view logic

query, decide, render

HttpResponse

always returned

  1. HttpRequest — always the first argument
  2. view logic — query, decide, render
  3. HttpResponse — always returned

Function-based vs. class-based views

Function-based vs. class-based views
PropertyFunction-based (FBV)Class-based (CBV)
Shapedef view(request): ...class View(View): def get(self, request): ...
Best forone-off, simple logiccommon patterns — list, detail, create/update
Reusevia plain function compositionvia subclassing and mixins
Registered in urls.py asviews.detailDetailView.as_view()

Together

python
# FBV
def article_list(request):
    articles = Article.objects.all()
    return render(request, "articles/list.html", {"articles": articles})

# equivalent CBV
from django.views.generic import ListView

class ArticleListView(ListView):
    model = Article
    template_name = "articles/list.html"

Remember: A view takes an HttpRequest and must return an HttpResponse on every code path — raise Http404 for "not found" rather than returning early with nothing.

See also: mtv architecture · url configuration · templates

Middleware

standardintermediate

Middleware is an ordered chain of hooks running on every request/response — MIDDLEWARE lists them top-to-bottom going in, reverse coming out. Each layer can inspect, modify, or short-circuit.

Think of it as

Middleware is an onion: the request enters through every outer layer to reach the view at the center, then the response exits back out through every layer it just entered, in reverse. AuthenticationMiddleware, for example, is a layer that adds request.user on the way in — it only works because SessionMiddleware, a layer outside it, already ran first.

python
def simple_middleware(get_response):
    # runs once, at server startup
    def middleware(request):
        # before the view / next layer
        response = get_response(request)
        # after the view / next layer
        return response
    return middleware

What we're doing: Write a minimal middleware that times a request and adds the duration as a response header, showing the before/after split around get_response().

mysite/timing_middleware.pypython
import time

def timing_middleware(get_response):
    def middleware(request):
        start = time.monotonic()
        response = get_response(request)
        elapsed = time.monotonic() - start
        response["X-Response-Time"] = f"{elapsed:.3f}s"
        return response
    return middleware
3
timing_middleware(get_response) is the factory — Django calls it once at startup, with get_response being the next layer inward.
5
Code before get_response(request) is the "on the way in" phase.
6
get_response(request) hands off to the next layer — eventually the view itself.
8
Code after get_response(request) is the "on the way out" phase — it runs after the view has already produced a response.

Why this works: Splitting each middleware into "before get_response" and "after get_response" is what makes the onion model work: the timer has to start before the view runs and read elapsed time after it returns, so it necessarily wraps get_response() rather than running entirely before or after the whole chain.

Placing a middleware that reads request.user before AuthenticationMiddleware

Wrong

python
MIDDLEWARE = [
    "myapp.middleware.log_user_middleware",   # reads request.user
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",  # sets it — too late
]

Better

python
MIDDLEWARE = [
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",  # sets request.user
    "myapp.middleware.log_user_middleware",   # now safe to read it
]

What you see: AttributeError: 'WSGIRequest' object has no attribute 'user' — or a silently-wrong AnonymousUser if the attribute happens to exist from an earlier request.

Why: Each middleware only sees what the layers placed BEFORE it in the list have already added to the request, because request-phase code runs top-to-bottom. A middleware reading request.user has to be listed after AuthenticationMiddleware, the same way AuthenticationMiddleware itself has to come after SessionMiddleware, whose session it reads.

The onion — request in, response out

SecurityMiddleware

outermost

SessionMiddleware

adds request.session

AuthenticationMiddleware

adds request.user

the view

innermost

  1. SecurityMiddleware — outermost
  2. SessionMiddleware — adds request.session
  3. AuthenticationMiddleware — adds request.user
  4. the view — innermost

Default MIDDLEWARE, in the order it runs

Default MIDDLEWARE, in the order it runs
MiddlewareWhat it adds
SecurityMiddlewareHTTPS redirects and security headers
SessionMiddlewarerequest.session
CommonMiddlewarebaseline URL handling
CsrfViewMiddlewareCSRF protection on unsafe methods
AuthenticationMiddlewarerequest.user — requires SessionMiddleware first
MessageMiddlewarethe one-time flash-message store

Together

python
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",
]

Remember: MIDDLEWARE order matters: request-phase runs top-to-bottom, response-phase runs bottom-to-top — each layer only sees what earlier ones already added.

See also: url configuration · sessions · authentication

Advertisement

Data and admin

The ORM layer, the auto-generated admin interface built on top of it, and scripted actions via manage.py.

Models

corebeginner

A model is a Python class subclassing django.db.models.Model — each attribute is a database field, each instance is one row. Django generates a query API (.objects.filter(), .save()) and, via migrations, the real table.

Think of it as

A model is a labelled shipping crate template: the class defines what compartments (fields) every crate of this kind has and what can go in each one (a CharField compartment only holds text, up to a max size). Building one crate (BlogPost.objects.create(...)) doesn't touch the template — it stamps out one new crate matching the shape, exactly like a class producing an object in classes and objects.

python
from django.db import models

class BlogPost(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

What we're doing: Define a model, then use its auto-generated manager API to create and query rows.

blog/models.pypython
from django.db import models

class BlogPost(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    is_published = models.BooleanField(default=False)

# usage, e.g. in a view or shell:
post = BlogPost.objects.create(title="Hello", content="First post", is_published=True)

published = BlogPost.objects.filter(is_published=True)
3
BlogPost(models.Model) — the class defines the shape of a row, three fields plus the automatic id.
9
.objects.create(...) builds and saves one new row in a single call — the class itself is never queried directly, only through .objects.
11
.objects.filter(...) returns a QuerySet — a lazy, database-backed collection matching the condition, not a plain Python list.

Why this works: The class body only declares shape — title, content, is_published — nothing about it queries or writes to the database until .objects is used, the same separation between "define the blueprint" and "build/query instances of it" that classes and objects establishes for plain Python classes.

Editing a model field and forgetting to migrate

Wrong

python
class BlogPost(models.Model):
    title = models.CharField(max_length=200)
    subtitle = models.CharField(max_length=200)  # added, but migrations never run
# BlogPost.objects.create(title="Hi", subtitle="...")  # OperationalError

Better

bash
# after editing models.py:
python manage.py makemigrations blog
python manage.py migrate

What you see: django.db.utils.OperationalError: no such column: blog_blogpost.subtitle — the model class and the actual database table have drifted apart.

Why: Editing models.py only changes the Python class; the database table is a separate, physical thing that Django updates exclusively through migration files. Skipping makemigrations/migrate leaves the ORM believing a column exists that the real table does not have.

From class to table to row

class BlogPost

defines fields

makemigrations + migrate

generates schema

blog_blogpost table

one column per field

BlogPost.objects.create(...)

one row

  1. class BlogPost — defines fields
  2. makemigrations + migrate — generates schema
  3. blog_blogpost table — one column per field
  4. BlogPost.objects.create(...) — one row

Common model field types

Common model field types
FieldStores
CharField(max_length=N)a short string, bounded length required
TextField()an unbounded block of text
IntegerField() / BooleanField()a whole number / True or False
DateTimeField(auto_now_add=True)a timestamp, set once at creation
ForeignKey(Other, on_delete=...)a many-to-one link to another model

Together

python
from django.db import models

class BlogPost(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    is_published = models.BooleanField(default=False)
    author = models.ForeignKey("auth.User", on_delete=models.CASCADE)

Remember: A model is a class (fields = columns); Model.objects is how you create/query rows; makemigrations + migrate is what actually changes the database schema.

See also: mtv architecture · admin · views

Admin

standardbeginner

The Django admin is an automatic, model-driven interface for managing data — register a model with admin.site.register() and you get a working create/read/update/delete UI at /admin/ with no view or template code of your own.

Think of it as

The admin reads a model the way the app registry reads INSTALLED_APPS: it introspects the class you already wrote (CharField, ForeignKey, ...) and builds a form and a list page from that, the same way an office generates a standard intake form once it knows a person's job title, rather than writing a bespoke form for every employee.

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

admin.site.register(Article)

What we're doing: Register a model with the bare minimum, then upgrade to a customized ModelAdmin.

articles/admin.pypython
from django.contrib import admin
from .models import Article

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "is_published"]
    list_filter = ["is_published"]
    search_fields = ["title"]
4
@admin.register(Article) connects the class below it to Article — equivalent to calling admin.site.register(Article, ArticleAdmin) afterward.
6
list_display controls the columns shown on the change list page — without it, only the object's __str__ shows.
7
list_filter adds a sidebar for narrowing the list by is_published, with no extra view code.

Why this works: Every one of these options customizes presentation of a model that already exists — none of them touch the database or add new fields, because the admin's whole value is generating an interface FROM the model you already defined, not defining a second, separate one just for admin.

Building the public site's UI directly on top of the admin

Wrong

text
"Customers will browse and edit their own orders
through /admin/, with permissions restricting what
they see."

Better

text
Build dedicated views/templates for customer-facing
pages; reserve /admin/ for trusted staff managing data
directly.

What you see: Fighting the admin's generated forms and permission model to make it behave like a customer-facing product, instead of building the small number of views actually needed.

Why: The docs explicitly scope the admin as an internal management tool for trusted staff, not a customizable public front end — its permission model, styling, and workflow assumptions are all built around that narrower use case, so stretching it to be the whole product UI usually costs more than writing a few purpose-built views.

Common ModelAdmin options

Common ModelAdmin options
OptionEffect
list_displaywhich fields show as columns on the change list
list_filteradds a sidebar filter for the named fields
search_fieldsenables a search box over the named fields
readonly_fieldsshows a field's value without allowing edits
prepopulated_fieldsauto-fills one field (e.g. a slug) from another as you type

Together

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

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "is_published"]
    list_filter = ["is_published"]
    search_fields = ["title"]

Remember: admin.site.register(Model) generates a CRUD interface from a model's fields — ModelAdmin customizes list_display, list_filter, search_fields.

See also: models · authentication · management commands

Management commands

standardintermediate

A management command is an action run through manage.py — Django ships runserver, migrate, and createsuperuser, and you can add your own by dropping a Command class (subclassing BaseCommand) into an app's management/commands/ directory.

Think of it as

manage.py is the project's own command-line tool, and every management command is one verb it understands — migrate, runserver, createsuperuser are built in, and your app can add new verbs the same way, just by placing a file in the right directory. Nothing registers a custom command explicitly; its location alone is what makes manage.py find it.

python
# polls/management/commands/closepoll.py
from django.core.management.base import BaseCommand

class Command(BaseCommand):
    help = "Closes the specified poll for voting"

    def add_arguments(self, parser):
        parser.add_argument("poll_ids", nargs="+", type=int)

    def handle(self, *args, **options):
        ...

What we're doing: Write a custom command that closes a poll by id, using add_arguments() for input and self.style for output.

polls/management/commands/closepoll.pypython
from django.core.management.base import BaseCommand, CommandError
from polls.models import Question as Poll

class Command(BaseCommand):
    help = "Closes the specified poll for voting"

    def add_arguments(self, parser):
        parser.add_argument("poll_ids", nargs="+", type=int)

    def handle(self, *args, **options):
        for poll_id in options["poll_ids"]:
            try:
                poll = Poll.objects.get(pk=poll_id)
            except Poll.DoesNotExist:
                raise CommandError(f'Poll "{poll_id}" does not exist')
            poll.opened = False
            poll.save()
            self.stdout.write(self.style.SUCCESS(f'Closed poll "{poll_id}"'))
4
class Command(BaseCommand) — this exact name, in this exact file location, is what manage.py discovers automatically.
7
add_arguments() defines the command's CLI arguments using the standard library's argparse.
10
handle() is the one required method — everything the command actually does goes here.

Why this works: Placing the file at polls/management/commands/closepoll.py is the entire registration mechanism — Django discovers commands by walking that directory structure across every installed app, so `python manage.py closepoll 1 2 3` works with no import or settings entry beyond the app itself being in INSTALLED_APPS.

Using print() instead of self.stdout.write()

Wrong

python
def handle(self, *args, **options):
    print("Poll closed")   # bypasses Django's output handling

Better

python
def handle(self, *args, **options):
    self.stdout.write(self.style.SUCCESS("Poll closed"))

What you see: Output appears fine when run manually, but breaks or gets lost when a caller redirects the command's output stream, or when running the command via call_command() from other Python code.

Why: self.stdout is a stream Django manages and can point elsewhere (call_command() lets callers substitute their own stdout to capture output) — plain print() always writes to the real process stdout regardless, ignoring that redirection entirely.

Frequently used built-in commands

Frequently used built-in commands
CommandDoes
migrateapplies pending database migrations
makemigrationsgenerates migration files from model changes
runserverstarts the development server
createsuperusercreates an admin-site login interactively
shellopens a Python shell with the project's settings loaded

Together

bash
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver

Remember: A management command is a Command(BaseCommand) with a handle() method at <app>/management/commands/<name>.py — file location registers it.

See also: app registry · models · admin

Advertisement

Presentation

Rendering HTML from data, validating user input, and decoupling side effects from the code that triggers them.

Templates

standardbeginner

The Django Template Language (DTL) renders HTML from a template file plus a context dict: `{{ variable }}` outputs a value, `{% tag %}` provides logic like loops and conditionals. render() (or render_to_string()) combines the two.

Think of it as

A template is a form letter with two kinds of blanks: `{{ }}` blanks get a value dropped straight in (like a mail-merge field), while `{% %}` blanks are instructions to the person assembling the letter (repeat this paragraph per item, skip this section if false) — the letter itself never decides WHAT goes in the blanks, that's the View's job, matching the separation MTV architecture describes.

python
from django.shortcuts import render

def story_detail(request, pk):
    story = Story.objects.get(pk=pk)
    return render(request, "news/story_detail.html", {"story": story})

What we're doing: Render a template that loops over related objects and conditionally shows content, driven entirely by the view's context dict.

news/story_detail.htmlhtml
<h1>{{ story.title }}</h1>
<p>By {{ story.author }}</p>
{% if story.published %}
  <p>{{ story.content }}</p>
{% else %}
  <p>This story is not yet published.</p>
{% endif %}
{% for comment in story.comments %}
  <p>{{ comment.text }} — {{ comment.author }}</p>
{% endfor %}
3
{% if story.published %} branches purely on a value already computed and passed in by the view — the template does not query anything.
8
{% for comment in story.comments %} iterates whatever iterable the view put in the context under that key.

Why this works: Every value the template touches — story, story.published, story.comments — was already resolved by the view before render() was called. The template only formats what it is handed, which is the same MTV separation of concerns that keeps a Template ignorant of HTTP and the database entirely.

Trying to run arbitrary Python inside a template

Wrong

html
{# DTL has no function calls or arbitrary expressions #}
<p>{{ story.comments.filter(approved=True).count() }}</p>

Better

python
# do the work in the view, pass the result in the context
def story_detail(request, pk):
    story = Story.objects.get(pk=pk)
    approved_count = story.comments.filter(approved=True).count()
    return render(request, "news/story_detail.html", {
        "story": story, "approved_count": approved_count,
    })

What you see: TemplateSyntaxError, or the tag/filter is simply not recognized — DTL syntax does not support calling a method with arguments or chaining arbitrary Python.

Why: The DTL deliberately excludes arbitrary code execution — a filter takes at most one static argument, and a method access with arguments is not expressible at all. That is a design choice (see MTV architecture's note on loose coupling), not a missing feature: logic like filtering belongs in the view, which is exactly why detail's example view does the same kind of pre-computation.

Core template syntax

Core template syntax
SyntaxPurpose
{{ value }}output a context variable
{{ value|filter }}transform a value, e.g. {{ name|upper }}
{% if cond %}...{% endif %}conditional rendering
{% for x in list %}...{% endfor %}loop over an iterable
{% load static %}load a tag library before using its tags

Together

html
{% load static %}
<h1>{{ story.title }}</h1>
{% if story.published %}
  <p>{{ story.content }}</p>
{% else %}
  <p>Not yet published.</p>
{% endif %}

Remember: {{ }} outputs a value, {% %} is a tag for logic; a template only formats what the view already put in context — it cannot query or run arbitrary Python.

See also: mtv architecture · views · static files

Forms

standardbeginner

A Form class describes an HTML form the same way a model describes a database table — fields as class attributes. Instantiate it with POST data, call is_valid(), then read validated, type-converted values from cleaned_data.

Think of it as

A Form is a bouncer with a checklist, not a display case. Handed raw, untrusted POST data, is_valid() runs every field's own check (a CharField enforces max_length, an EmailField enforces email shape) and only lets validated, converted values through into cleaned_data — invalid submissions are turned away with field-specific errors, never silently accepted.

python
from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    message = forms.CharField(widget=forms.Textarea)
    sender = forms.EmailField()
    cc_myself = forms.BooleanField(required=False)

What we're doing: Handle both GET (show a blank form) and POST (validate submitted data) with the same Form class in one view.

contact/views.pypython
from django.shortcuts import render, redirect
from .forms import ContactForm

def contact(request):
    if request.method == "POST":
        form = ContactForm(request.POST)
        if form.is_valid():
            subject = form.cleaned_data["subject"]
            sender = form.cleaned_data["sender"]
            return redirect("thanks")
    else:
        form = ContactForm()
    return render(request, "contact.html", {"form": form})
6
ContactForm(request.POST) is bound to the submitted data — the same class used unbound on line 11 for a fresh GET request.
7
is_valid() must be called before cleaned_data is trustworthy — it runs every field's validator and collects errors.
8
cleaned_data["subject"] is the validated, converted value — not the raw request.POST["subject"] string.

Why this works: The same ContactForm class handles both branches because a Form is just a description of fields and their validation rules — whether it is "unbound" (line 11, nothing to validate yet) or "bound" (line 6, data attached) is a property of the instance, not something requiring two different classes.

Reading request.POST directly instead of cleaned_data

Wrong

python
form = ContactForm(request.POST)
if form.is_valid():
    subject = request.POST["subject"]   # bypasses validation/conversion

Better

python
form = ContactForm(request.POST)
if form.is_valid():
    subject = form.cleaned_data["subject"]   # validated + converted

What you see: A BooleanField or IntegerField comes back as a raw string ("on", "42") instead of True or 42, and any validation the field defined (max_length, a custom clean_ method) never actually filtered the value being used.

Why: request.POST holds only raw strings straight from the HTTP request — is_valid() is what runs each field's type conversion and validators and puts the RESULT in cleaned_data. Reading request.POST directly after is_valid() still passed means none of that validation or conversion work actually gets used.

Bound vs. unbound forms

Bound vs. unbound forms
PropertyUnboundBound
Created asContactForm()ContactForm(request.POST)
Typical useGET — show a blank formPOST — validate submitted data
is_valid()always FalseTrue only if every field passes
cleaned_datanot availableavailable once is_valid() returns True

Together

python
if request.method == "POST":
    form = ContactForm(request.POST)   # bound
    if form.is_valid():
        subject = form.cleaned_data["subject"]
else:
    form = ContactForm()               # unbound

Remember: A Form describes fields like a model describes columns; call is_valid() first, then read values from cleaned_data, never straight from request.POST.

See also: views · templates · models

Signals

standardintermediate

Signals let a sender notify a set of receivers that something happened, without either side importing the other directly. post_save fires after any model instance saves; @receiver connects a plain function to listen for it.

Think of it as

A signal is a building's fire alarm, not a phone call. The thing that pulls it (post_save firing after User.save()) doesn't know or care who is listening — it just broadcasts. Any number of receivers can be listening (send a welcome email, invalidate a cache, log an audit event) without the save() call itself knowing any of them exist, which is exactly the decoupling that reaching-for-a-signal buys over calling those functions directly.

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

@receiver(post_save, sender=MyModel)
def on_save(sender, instance, created, **kwargs):
    ...

What we're doing: Send a welcome email exactly once, when a User is first created — not on every subsequent save.

accounts/signals.pypython
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User

@receiver(post_save, sender=User)
def notify_user_created(sender, instance, created, **kwargs):
    if created:
        send_mail("Welcome!", f"Welcome {instance.username}",
                   "from@example.com", [instance.email])
3
sender=User restricts this receiver to only fire for User saves, not every model in the project.
5
created is True only on the INSERT that first creates the row — False on every later .save() that updates it.
6
if created guards the email so an existing user editing their profile does not get "welcomed" again.

Why this works: post_save fires on every save — insert AND update — so `created` is the only way to tell them apart from inside the receiver. Checking it is what makes this a "welcome new user" signal instead of a "spam every save" signal.

Connecting a receiver at module import time instead of in ready()

Wrong

python
# accounts/apps.py
from . import signals   # imported directly at module load — too early

class AccountsConfig(AppConfig):
    name = "accounts"

Better

python
# accounts/apps.py
class AccountsConfig(AppConfig):
    name = "accounts"

    def ready(self):
        from . import signals   # safe — registry is fully populated

What you see: AppRegistryNotReady, or the receiver simply never connects because signals.py was imported before Django finished setting up.

Why: Connecting receivers is exactly the kind of startup work the app registry describes AppConfig.ready() as existing for — it only runs once every app's models are guaranteed importable, so a receiver referencing another app's model (like User here) is safe to import at that point but not earlier.

Frequently used built-in signals

Frequently used built-in signals
SignalFires
pre_save / post_savejust before / after a model instance is saved
pre_delete / post_deletejust before / after a model instance is deleted
m2m_changedwhen a ManyToManyField relation changes
request_finishedafter Django finishes processing an HTTP request

Together

python
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User

@receiver(post_save, sender=User)
def notify_user_created(sender, instance, created, **kwargs):
    if created:
        send_welcome_email(instance)

Remember: A signal lets a sender notify receivers without either side importing the other — connect in AppConfig.ready(), check `created` for insert vs. update.

See also: app registry · models · authentication

Advertisement

Files and users

Serving the files a project ships with, handling what users upload, and identifying who is asking.

Static files

standardbeginner

Static files are the CSS, JS, and images shipped with the project, not user uploads. STATIC_URL prefixes their URLs, {% static %} builds the path, collectstatic gathers them for production.

Think of it as

Static files are the parts of a store's fixtures you set up once before opening — shelving, signage, the checkout counter. They ship with the project and never change per-visitor, unlike media files (customer photos on a loyalty wall), which arrive AFTER the store opens, from the outside. In development runserver serves them for free; in production a real web server takes over, which is what collectstatic prepares for.

html
{% load static %}
<img src="{% static 'blog/logo.png' %}" alt="Logo">

What we're doing: Reference an app-namespaced static file from a template, then prepare it for production with collectstatic.

blog/templates/blog/post.htmlhtml
{% load static %}
<link rel="stylesheet" href="{% static 'blog/post.css' %}">
<img src="{% static 'blog/hero.jpg' %}" alt="Post hero image">
1
{% load static %} must appear before any {% static %} tag is used in the template.
2
'blog/post.css' resolves against blog/static/blog/post.css — the app-name subfolder prevents two apps' files from colliding.

Why this works: Namespacing each app's static files under its own name (blog/static/blog/...) is what lets two different apps each ship a file literally named logo.png without one silently overwriting the other once collectstatic merges everything into one STATIC_ROOT directory.

Relying on runserver to serve static files in production

Wrong

bash
# DEBUG = False, deployed with:
python manage.py runserver 0.0.0.0:8000

Better

bash
python manage.py collectstatic
# then configure nginx/Apache to serve STATIC_ROOT directly,
# and run the app itself behind a real WSGI/ASGI server

What you see: CSS and images 404 once DEBUG is set to False, even though everything worked in development.

Why: Django's automatic static-file serving via runserver is explicitly a development convenience gated on DEBUG = True — the docs call serving files this way in production "grossly inefficient and probably insecure." Production static serving is collectstatic's job, handed off to a real web server.

Static file settings

Static file settings
SettingMeaning
STATIC_URLthe URL prefix, e.g. "static/"
STATICFILES_DIRSextra directories to search, beyond each app's static/
STATIC_ROOTwhere collectstatic copies everything for production

Together

python
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"

Remember: Static files (CSS/JS/images) are namespaced per-app, referenced via {% static %}, auto-served only when DEBUG=True, gathered by collectstatic.

See also: templates · media files · settings

Media files

standardbeginner

Media files are content users upload — photos, attachments — unlike static files, which ship with the project. FileField/ImageField handle the upload; MEDIA_ROOT is where they land, MEDIA_URL serves them.

Think of it as

If static files are a store's fixed shelving, media files are what customers actually put ON the shelves after the store opens — different per visitor, arriving over time, and never known in advance. A FileField/ImageField on a model is the shelf slot; upload_to decides which subdirectory a given upload lands in.

python
from django.db import models

class Car(models.Model):
    name = models.CharField(max_length=255)
    photo = models.ImageField(upload_to="cars")

What we're doing: Define a model that accepts an uploaded photo, and show the three ways to read back where it ended up.

inventory/models.pypython
from django.db import models

class Car(models.Model):
    name = models.CharField(max_length=255)
    photo = models.ImageField(upload_to="cars")
    specs = models.FileField(upload_to="specs")
5
ImageField(upload_to="cars") — Django writes uploads under MEDIA_ROOT/cars/ and validates the upload is actually an image.
6
FileField accepts any file type — use it when there's no need to validate the upload is an image specifically.

Why this works: upload_to keeps different kinds of uploads separated on disk (cars/ vs specs/) the same way an app's static/<app_name>/ namespacing keeps static files from colliding — the same organizing idea, applied to content that arrives at runtime instead of being shipped with the code.

Serving MEDIA_ROOT with Django itself in production

Wrong

python
# urls.py, still present after DEBUG = False
from django.conf import settings
from django.conf.urls.static import static

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Better

text
Serve MEDIA_ROOT via the web server (nginx/Apache) or
object storage (e.g. S3) in production — Django's own
static() helper is documented as development-only.

What you see: User uploads work in development but load slowly, insecurely, or not at all once deployed behind a real web server.

Why: django.conf.urls.static.static() exists specifically for local development convenience — serving arbitrary user-uploaded files through the Python process itself does not scale and skips the access controls a real web server or object store provides, the same production concern static files' collectstatic entry raises for a different reason.

Accessing an uploaded file from a model instance

Accessing an uploaded file from a model instance
AttributeGives
instance.photo.namethe stored relative path, e.g. "cars/chevy.jpg"
instance.photo.paththe absolute filesystem path
instance.photo.urlthe public URL to serve it, under MEDIA_URL

Together

python
car = Car.objects.get(name="57 Chevy")
print(car.photo.name)   # 'cars/chevy.jpg'
print(car.photo.path)   # '/srv/media/cars/chevy.jpg'
print(car.photo.url)    # '/media/cars/chevy.jpg'

Remember: Media files are user uploads (FileField/ImageField) under MEDIA_ROOT, served from MEDIA_URL — distinct from project-shipped static files.

See also: static files · models · settings

Authentication

standardintermediate

Django's auth system provides a User model, authenticate() to check credentials, login()/logout() to attach/clear a session, and request.user on every request. @login_required guards a view.

Think of it as

authenticate() and login() are two separate steps on purpose, like checking an ID at the door versus actually stamping a wristband: authenticate() only verifies a username/password pair and hands back a User or None, doing nothing else — login() is the separate step that actually attaches that user to the current session, which is why you always see the two called one after the other rather than combined.

python
from django.contrib.auth.decorators import login_required

@login_required
def dashboard(request):
    return render(request, "dashboard.html", {"user": request.user})

What we're doing: Write a login view that authenticates then logs in, and a protected view guarded by login_required.

accounts/views.pypython
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render

def login_view(request):
    if request.method == "POST":
        username = request.POST["username"]
        password = request.POST["password"]
        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            return redirect("dashboard")
    return render(request, "login.html")

@login_required
def dashboard(request):
    return render(request, "dashboard.html", {"user": request.user})
9
authenticate() only checks the credentials — nothing about the request or session changes yet, and the return value could still be None.
10
login(request, user) is the separate call that actually attaches this user to request.session — this is the step that makes future requests see them as logged in.
15
@login_required wraps dashboard — an unauthenticated visitor is redirected to LOGIN_URL instead of ever reaching the view body.

Why this works: authenticate() and login() being two calls rather than one lets a view check credentials without necessarily starting a session — useful for anything that needs "is this password right?" without "log this person in", which login() alone commits to.

Calling login() without checking authenticate()'s return value

Wrong

python
user = authenticate(request, username=username, password=password)
login(request, user)   # user might be None here!

Better

python
user = authenticate(request, username=username, password=password)
if user is not None:
    login(request, user)
else:
    # show an invalid-credentials error

What you see: TypeError, or a session that appears to log in an anonymous/invalid user — login()'s behavior when passed None is not what a real successful login looks like.

Why: authenticate() returns None for any failed check — wrong password, unknown username, an inactive account — and login() has no built-in guard against being called with that None. The `if user is not None` check is not optional error handling; it is the only thing separating "credentials were valid" from "credentials were rejected."

The core auth functions

The core auth functions
CallDoes
authenticate(request, username=, password=)checks credentials, returns User or None
login(request, user)attaches user to request.session
logout(request)clears the session
request.user.is_authenticatedTrue for a real user, False for AnonymousUser

Together

python
from django.contrib.auth import authenticate, login

user = authenticate(request, username="ada", password="secret")
if user is not None:
    login(request, user)

Remember: authenticate() checks credentials, returns User or None; login(request, user) is the separate step that starts the session — always check for None first.

See also: sessions · middleware · admin

Advertisement

Sessions and locale

Per-visitor state across requests, one-time notifications, and adapting to language and time zone.

Sessions

standardintermediate

Django's session framework stores per-visitor data server-side, identified via a session ID cookie. SessionMiddleware makes request.session behave like a dict; SESSION_ENGINE picks where data lives.

Think of it as

A session is a coat-check ticket, not a coat carried around by the visitor. The cookie in the visitor's browser holds only a claim ticket (the session ID) — the actual data (the coat) stays on the server, in whichever storage SESSION_ENGINE names. That's the opposite of a naive 'store everything in the cookie' approach, and it's why session data is not size-limited by cookie size the way a JWT-in-a-cookie would be.

python
def post_comment(request, new_comment):
    if request.session.get("has_commented", False):
        return HttpResponse("You've already commented.")
    request.session["has_commented"] = True
    return HttpResponse("Thanks!")

What we're doing: Use request.session to prevent the same visitor from submitting a comment twice, without requiring them to be logged in.

comments/views.pypython
def post_comment(request, new_comment):
    if request.session.get("has_commented", False):
        return HttpResponse("You've already commented.")
    Comment.objects.create(text=new_comment)
    request.session["has_commented"] = True
    return HttpResponse("Thanks for your comment!")
2
request.session.get(..., False) reads with a default the same way a plain dict would — no key means "never set", treated as False.
5
Setting request.session["has_commented"] = True persists across this visitor's future requests, keyed by their session cookie.

Why this works: This works for anonymous visitors too, since a session exists independent of authentication (see authentication) — request.session is available as soon as SessionMiddleware runs, whether or not the visitor has ever logged in.

Assuming session data is stored in the cookie itself

Wrong

text
"I'll store a large shopping cart directly in
request.session — it's just a cookie, so it's cheap."

Better

text
With the default db/cache backends, session data lives
server-side — the cookie only carries a session ID, so
size in the session is not a cookie-size concern at all.

What you see: Confusion about session size limits, or (with SESSION_ENGINE set to signed_cookies specifically) actual failures once the data grows past what a cookie can hold.

Why: Only the signed_cookies backend actually puts session data in the cookie — every other backend (db, cache, cached_db, file, the default) stores the real data server-side and sends only a session ID in the cookie. The size and content concerns are completely different depending on which backend is configured.

SESSION_ENGINE backends

SESSION_ENGINE backends
BackendStores data in
django.contrib.sessions.backends.dbthe django_session database table (default)
django.contrib.sessions.backends.cachethe configured cache only — fastest, not durable
django.contrib.sessions.backends.cached_dbcache first, database as a write-through backup
django.contrib.sessions.backends.signed_cookiesthe cookie itself, cryptographically signed

Together

python
request.session["cart_id"] = 42
cart_id = request.session.get("cart_id")
request.session.set_expiry(300)   # expire in 5 minutes

Remember: request.session behaves like a dict; the cookie holds only a session ID, not the data — SESSION_ENGINE decides where it lives.

See also: middleware · authentication · messages framework

Messages framework

standardbeginner

The messages framework carries one-time "flash" notifications from one request to the next — messages.success(request, "Saved!") in a view, then {% for message in messages %} in the next template.

Think of it as

A message is a sticky note left on a colleague's desk before you leave, not a live conversation. You add it (messages.success(...)) during one request — often right before a redirect — and it waits, stored via cookie/session, until the NEXT request's template reads and displays it, at which point it's used up and gone, exactly like a note is thrown away once read.

html
{% if messages %}
<ul class="messages">
  {% for message in messages %}
    <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
  {% endfor %}
</ul>
{% endif %}

What we're doing: Queue a success message in a view right before a redirect, then render it in the template the following request loads.

profile/views.pypython
from django.contrib import messages
from django.shortcuts import redirect

def update_profile(request):
    # ... save the form ...
    messages.success(request, "Profile details updated.")
    return redirect("profile")
5
messages.success() queues the message during THIS request — it is not rendered here, since a redirect follows immediately.
6
The redirect sends a fresh request to "profile", whose template is where {% for message in messages %} will actually display it.

Why this works: The redirect-then-display pattern is exactly why messages need to survive across requests in the first place — a plain context variable would be lost the instant the redirect happens, since a redirect is a brand-new request with no memory of the one before it. Messages are stored (cookie/session) specifically to bridge that gap.

Adding a message but never iterating it in a template

Wrong

html
{# base.html — messages queued but nothing ever renders them #}
<body>
  {% block content %}{% endblock %}
</body>

Better

html
<body>
  {% for message in messages %}
    <div class="{{ message.tags }}">{{ message }}</div>
  {% endfor %}
  {% block content %}{% endblock %}
</body>

What you see: messages.success() appears to do nothing — no error is raised, the message is simply queued forever and never shown.

Why: A message only leaves storage once a template actually iterates the `messages` context variable — that iteration is what marks it as read and clears it. Without a {% for %} loop somewhere in the rendered page, every queued message just accumulates silently in storage.

Message-level shortcut functions

Message-level shortcut functions
CallTypical use
messages.debug(request, text)developer-only diagnostic notes
messages.info(request, text)neutral information
messages.success(request, text)confirms an action succeeded
messages.warning(request, text)a non-fatal issue worth flagging
messages.error(request, text)an action failed

Together

python
from django.contrib import messages

messages.success(request, "Profile updated.")
messages.error(request, "Could not save — try again.")

Remember: messages.success/error(request, text) queues a one-time message; {% for message in messages %} displays AND clears it — classic pairing is queue-then-redirect.

See also: sessions · forms · templates

Internationalization

standardintermediate

Internationalization (i18n) marks text translatable — gettext()/_() in Python, {% trans %} in templates — so translators supply per-language files later. LocaleMiddleware picks a language per request.

Think of it as

i18n is leaving blanks in a script for a dub studio to fill in later, not writing the script in every language up front. Wrapping a string in _("Welcome") doesn't translate it — it marks the line as one a translator will eventually supply a version of, in a .po file, entirely separate from the Python or template code that displays it.

python
# settings.py
USE_I18N = True
LANGUAGE_CODE = "en-us"

MIDDLEWARE = [
    # ...
    "django.middleware.locale.LocaleMiddleware",
]

What we're doing: Mark a message translatable in both Python and a template, using the same underlying gettext mechanism.

pages/views.pypython
from django.utils.translation import gettext as _
from django.shortcuts import render

def homepage(request):
    greeting = _("Welcome to my site")
    return render(request, "home.html", {"greeting": greeting})

# home.html:
# {% load i18n %}
# <h1>{{ greeting }}</h1>
# <p>{% trans "Thanks for visiting." %}</p>
1
gettext as _ is the conventional alias — _("...") marks a string for extraction into a .po file.
5
The greeting is translated in Python and passed through context, while a second string is marked directly in the template via {% trans %} — both feed the same translation catalogue.

Why this works: Both mechanisms exist because a string can originate on either side of the MTV split (mtv-architecture) — some translatable text is computed in a view, some is static markup living entirely in a template — and i18n has to cover both without forcing everything through one layer.

Using gettext (not gettext_lazy) for a module-level or class-attribute string

Wrong

python
from django.utils.translation import gettext as _

class ContactForm(forms.Form):
    subject = forms.CharField(label=_("Subject"))  # evaluated once, at import time

Better

python
from django.utils.translation import gettext_lazy as _

class ContactForm(forms.Form):
    subject = forms.CharField(label=_("Subject"))  # evaluated when actually rendered

What you see: The label is always translated into whichever language was active when the module was first imported — every visitor sees the same language regardless of their own Accept-Language.

Why: Plain gettext() translates immediately, at whatever time the line of code runs — for a class attribute, that's once, at import time, long before any specific visitor's request exists. gettext_lazy() instead returns a lazy object that defers translation until the string is actually used (e.g. rendered), by which point LocaleMiddleware has set the correct per-request language.

Marking translatable text, by location

Marking translatable text, by location
LocationSyntax
Python codefrom django.utils.translation import gettext as _ _("Welcome")
Template{% load i18n %}{% trans "Welcome" %}
Lazy (module-level / class attribute)gettext_lazy — evaluated at render time, not import time

Together

python
from django.utils.translation import gettext as _

def homepage(request):
    message = _("Welcome to my site")
    return render(request, "home.html", {"message": message})

Remember: gettext/_() (Python) and {% trans %} (templates) mark text translatable; use gettext_lazy for anything evaluated at import time, not request time.

See also: templates · middleware · time zone support

Time zone support

standardintermediate

With USE_TZ = True (default), Django stores every datetime in UTC and converts only for display. timezone.now() returns an aware "now"; datetime.datetime.now() returns an unsafe naive one.

Think of it as

Storing everything in UTC is like a company keeping every meeting on one master calendar in headquarters' time zone, converting to each employee's local time only on the screen they're looking at. The stored, canonical value never changes based on who's viewing it — only the display does — which is exactly what avoids daylight-saving-time bugs that a naive per-user local timestamp would be prone to.

python
# settings.py
USE_TZ = True
TIME_ZONE = "UTC"

# anywhere in application code:
from django.utils import timezone
now = timezone.now()

What we're doing: Record a timestamp the correct way, then activate a per-request time zone so it displays converted for one visitor without altering the stored value.

orders/views.pypython
from django.utils import timezone
import zoneinfo

def place_order(request):
    order = Order.objects.create(placed_at=timezone.now())  # stored in UTC
    return redirect("order-detail", pk=order.pk)

class TimezoneMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        tzname = request.session.get("django_timezone")
        if tzname:
            timezone.activate(zoneinfo.ZoneInfo(tzname))
        return self.get_response(request)
4
timezone.now() — the value actually saved to the database is UTC, regardless of who placed the order or from where.
8
TimezoneMiddleware activates a per-request zone for DISPLAY purposes only — it never changes what was stored in placed_at.

Why this works: Splitting "what is stored" (always UTC, via timezone.now()) from "what is displayed" (converted per-request, via timezone.activate()) is the whole point of USE_TZ = True — order.placed_at means the same instant in time everywhere, and only its on-screen rendering changes based on the active zone, matching the middleware chain's general pattern of adding per-request context.

Mixing datetime.datetime.now() into a USE_TZ = True project

Wrong

python
import datetime
order.placed_at = datetime.datetime.now()   # naive
order.save()

Better

python
from django.utils import timezone
order.placed_at = timezone.now()   # aware, UTC
order.save()

What you see: RuntimeWarning: DateTimeField Order.placed_at received a naive datetime while time zone support is active — and comparisons between this value and any aware datetime later raise a TypeError.

Why: With USE_TZ = True, Django expects every datetime touching a DateTimeField to be aware. datetime.datetime.now() returns a naive one with no tzinfo at all, which Django cannot safely place on the UTC timeline — it does not know what zone the naive value was even meant to represent.

Aware vs. naive datetimes

Aware vs. naive datetimes
PropertyAwareNaive
Has tzinfo?yesno
Produced bydjango.utils.timezone.now()datetime.datetime.now()
Safe with USE_TZ = True?yes — this is what Django expectsno — raises a RuntimeWarning
Comparable to each other?aware to aware onlynaive to naive only

Together

python
from django.utils import timezone

now = timezone.now()             # aware, UTC
print(now.tzinfo)                # datetime.timezone.utc

Remember: USE_TZ=True stores datetimes in UTC, converts only for display; use timezone.now(), never datetime.datetime.now(), which returns an unsafe naive value.

See also: models · internationalization · settings

Advertisement