Django quick reference

312 entries — one card per concept, for looking something up rather than learning it. Each links back to the full explanation.

312

Django Fundamentals — Core concepts

22

pip install django && django-admin startproject mysite

Django is a batteries-included Python web framework — routing, ORM, admin, auth, and templates, generated as a working skeleton from one command.

django-admin startproject mysite cd mysite && python manage.py runserver

djangobasics
What Django is

INSTALLED_APPS = ["django.contrib.admin", "django.contrib.auth", ...]

Django ships an ORM, admin site, auth, sessions, and templating together, designed to interlock rather than picked and glued together separately.

INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", ]

Model (data) → View (logic) → Template (presentation)

Django's MTV pattern: Model holds data and rules, View reads the request and coordinates, Template renders output — roughly MVC with the View/Controller names swapped.

def article_detail(request, pk): article = Article.objects.get(pk=pk) return render(request, "detail.html", {"article": article})

djangoarchitecture
MTV architecture

django-admin startproject mysite · python manage.py startapp blog

A project is the whole site (one settings module); an app is a self-contained, reusable feature package registered in INSTALLED_APPS.

python manage.py startapp blog # then add "blog" to INSTALLED_APPS

from django.apps import apps; apps.get_model("blog", "Post")

The app registry is Django's in-memory catalogue of installed apps and models, populated once at startup — AppConfig.ready() is the safe place for cross-app imports.

class BlogConfig(AppConfig): name = "blog" def ready(self): from . import signals

from django.conf import settings

Settings are a plain Python module selected via DJANGO_SETTINGS_MODULE; read values through django.conf.settings, never by importing the module directly.

from django.conf import settings if settings.DEBUG: ...

djangoconfiguration
Settings

path("articles/<int:year>/", views.year_archive)

A URLconf maps URL patterns to views via path()/include(); ROOT_URLCONF names the root module, and the first matching pattern wins.

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

def view(request, ...): return HttpResponse(...)

A view is a callable taking an HttpRequest and returning an HttpResponse — function-based for simple logic, class-based for common reusable patterns.

def detail(request, pk): article = Article.objects.get(pk=pk) return render(request, "detail.html", {"article": article})

djangoviews
Views

def middleware(get_response): def wrapper(request): ...; return wrapper

Middleware is an ordered chain wrapping get_response — request-phase code runs top-to-bottom on the way in, response-phase code runs bottom-to-top on the way out.

def timing_middleware(get_response): def middleware(request): response = get_response(request) return response return middleware

djangomiddleware
Middleware

class Name(models.Model): field = models.CharField(max_length=N)

A model is a Python class mapping to a database table; makemigrations + migrate turns field changes into real schema changes; Model.objects is the query entry point.

class BlogPost(models.Model): title = models.CharField(max_length=200) BlogPost.objects.filter(title="Hello")

djangoorm
Models

@admin.register(Model) class ModelAdmin(admin.ModelAdmin): ...

The admin site auto-generates a CRUD interface from a registered model; ModelAdmin customizes list_display, list_filter, and search_fields.

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

djangoadmin
Admin

<app>/management/commands/<name>.py — class Command(BaseCommand): def handle(self, *args, **options): ...

A custom management command is a Command(BaseCommand) subclass discovered by file location, run via python manage.py <name>.

class Command(BaseCommand): def handle(self, *args, **options): self.stdout.write(self.style.SUCCESS("done"))

render(request, "template.html", {"key": value})

The Django Template Language renders {{ variables }} and {% tags %} against a context dict passed in by the view — it cannot run arbitrary Python.

{% if story.published %} {{ story.content }} {% endif %}

djangotemplates
Templates

form = MyForm(request.POST); if form.is_valid(): form.cleaned_data["field"]

A Form class validates submitted data via is_valid(); read converted, validated values from cleaned_data afterward, never straight from request.POST.

class ContactForm(forms.Form): subject = forms.CharField(max_length=100) sender = forms.EmailField()

djangoforms
Forms

@receiver(post_save, sender=Model) def handler(sender, instance, created, **kwargs): ...

Signals decouple senders from receivers — post_save fires after every save; check the `created` kwarg to distinguish insert from update.

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

djangosignals
Signals

{% load static %}{% static "app/file.css" %}

Static files (CSS/JS/images shipped with the app, not user uploads) are namespaced per-app and referenced via {% static %}; collectstatic gathers them for production.

{% load static %} <link rel="stylesheet" href="{% static 'blog/post.css' %}">

djangostatic
Static files

photo = models.ImageField(upload_to="cars")

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

car.photo.url # '/media/cars/chevy.jpg'

djangofiles
Media files

user = authenticate(request, username=, password=); if user: login(request, user)

Django auth: authenticate() verifies credentials and returns User or None; login(request, user) is the separate step that attaches the user to the session.

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

request.session["key"] = value

Sessions store per-visitor data server-side (SESSION_ENGINE), identified by a session-ID cookie; request.session behaves like a plain dict.

request.session["cart_id"] = 42 request.session.get("cart_id")

djangosessions
Sessions

messages.success(request, "text")

The messages framework queues one-time flash notifications that survive a redirect; a template iterating {% for message in messages %} both displays and clears them.

messages.success(request, "Saved!") return redirect("profile")

from django.utils.translation import gettext as _

i18n marks text translatable via gettext/_() in Python or {% trans %} in templates; translators later supply per-language .po files.

message = _("Welcome to my site")

from django.utils import timezone; timezone.now()

USE_TZ=True stores datetimes in UTC and converts for display; timezone.now() returns an aware UTC value — datetime.datetime.now() returns an unsafe naive one.

order.placed_at = timezone.now()

djangotimezones
Time zone support

Django Project Structure

15

python manage.py <command>

manage.py is django-admin pre-configured for this project — DJANGO_SETTINGS_MODULE is set automatically, so no --settings flag is needed.

python manage.py runserver python manage.py migrate

djangocli
manage.py

mysite/mysite/settings.py

settings.py is the project's central configuration file — INSTALLED_APPS, MIDDLEWARE, DATABASES — often split into base/dev/production files as a project grows.

# production.py from .base import * DEBUG = False

djangostructure
settings.py

urlpatterns = [path("billing/", include("billing.urls"))]

The project's root urls.py (ROOT_URLCONF) stays short — mostly include() per app — pushing actual routes into each app's own urls.py.

path("billing/", include("billing.urls"))

application = get_asgi_application()

asgi.py exposes an `application` callable that ASGI servers (uvicorn, daphne) use to serve the project — not used by runserver.

uvicorn mysite.asgi:application

djangodeployment
asgi.py

application = get_wsgi_application()

wsgi.py exposes an `application` callable that WSGI servers (gunicorn, uWSGI) use to serve the project in production.

gunicorn mysite.wsgi:application

djangodeployment
wsgi.py

class MyAppConfig(AppConfig): name = "myapp"

apps.py holds one AppConfig subclass per app, generated by startapp — name is required and must match the app's real import path.

class BillingConfig(AppConfig): name = "billing" verbose_name = "Billing & Invoices"

djangoapps
App apps.py

<app>/models.py

One models.py per app, generated empty by startapp — every model class the app owns goes here; other files import from it, never the reverse.

from django.db import models class Invoice(models.Model): amount = models.DecimalField(max_digits=10, decimal_places=2)

djangostructure
App models.py

<app>/views.py

views.py is generated empty per app — every view lives here, imports models from .models, and is referenced by name from that app's urls.py.

def invoice_list(request): invoices = Invoice.objects.all() return render(request, "billing/list.html", {"invoices": invoices})

djangostructure
App views.py

admin.site.register(Model)

admin.py starts nearly empty — a model is invisible in /admin/ until explicitly registered here, no error if forgotten.

from .models import Invoice admin.site.register(Invoice)

djangoadmin
App admin.py

class MyTestCase(TestCase): def test_x(self): ...

django.test.TestCase wraps each test in a rolled-back transaction — always subclass it, not plain unittest.TestCase, for tests touching the database.

python manage.py test billing

djangotesting
App tests.py

app_name = "billing" urlpatterns = [path("", views.list, name="list")]

An app's own urls.py is hand-added (not generated) — app_name namespaces its route names for {% url 'app:name' %} across apps.

{% url 'billing:list' %}

djangorouting
App urls.py

python manage.py makemigrations && python manage.py migrate

migrations/ holds numbered files describing schema changes, generated from models.py — commit them; never delete an already-applied one.

billing/migrations/0002_invoice_due_date.py

djangomigrations
migrations/

<app>/templates/<app_name>/file.html

Django discovers templates via APP_DIRS (each app's own templates/) plus TEMPLATES DIRS (project-wide) — namespace under templates/<app_name>/ to avoid collisions.

render(request, "billing/detail.html", {...})

djangotemplates
templates/

<app>/static/<app_name>/file.css

static/ discovers per-app automatically, plus STATICFILES_DIRS for project-wide assets — namespace under static/<app_name>/, mirroring templates/.

{% static 'billing/invoice.css' %}

djangostatic
static/

<app>/management/commands/<name>.py

management/commands/ needs __init__.py at both the management/ and commands/ levels — each other .py file becomes a command named after itself.

billing/management/commands/closepoll.py → python manage.py closepoll

Django App Registry

2

ForeignKey("otherapp.Model", on_delete=...)

Startup runs three full passes over INSTALLED_APPS (configs, then models, then ready()) — a string model reference sidesteps import order entirely.

order = models.ForeignKey("orders.Order", on_delete=models.CASCADE)

from django.utils.translation import gettext_lazy as _

Never query the database or call apps.get_model() at import time — it can run before the registry (or the database) is ready, raising AppRegistryNotReady.

title = models.CharField(max_length=200, verbose_name=_("title"))

Settings and Configuration — Core settings

3

DATABASES = {"default": {"ENGINE": "...", "NAME": "..."}}

INSTALLED_APPS/MIDDLEWARE/ROOT_URLCONF/TEMPLATES/DATABASES wire together what exists and where data lives; DATABASES needs a "default" key.

MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", ... ]

STATIC_ROOT = BASE_DIR / "staticfiles"

STATIC_ROOT/MEDIA_ROOT are real filesystem directories; STATIC_URL/MEDIA_URL are just the public URL prefixes mapping to them.

STATIC_URL = "static/" STATIC_ROOT = BASE_DIR / "staticfiles"

CACHES = {"default": {"BACKEND": "...RedisCache", "LOCATION": "redis://..."}}

CACHES defaults to a per-process LocMemCache — needs a shared backend like Redis once more than one worker process is running.

EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

Settings and Configuration — Security settings

2

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

SECRET_KEY signs sessions/tokens and must never be committed; DEBUG must be False in production; ALLOWED_HOSTS/CSRF_TRUSTED_ORIGINS reject the wrong host.

ALLOWED_HOSTS = ["example.com"] CSRF_TRUSTED_ORIGINS = ["https://example.com"]

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

SECURE_SSL_REDIRECT/HSTS settings enforce HTTPS; SECURE_PROXY_SSL_HEADER makes Django trust a proxy header — dangerous unless the proxy strictly overwrites it.

SECURE_HSTS_SECONDS = 3600 # start short, raise once confirmed

Settings and Configuration — Configuration strategy

1

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

Secrets come from the environment or a secret manager, never a settings.py literal — a missing required value should raise, not silently default.

DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True" # safe default

djangosettingsconfiguration
Configuration strategy

URL Routing

4

re_path(r"^articles/(?P<year>[0-9]{4})/$", views.year_archive)

re_path() matches with a regex; every captured group arrives as a string — use it only when path()'s converters can't express the pattern.

re_path(r"^reports/(?P<start_year>[0-9]{4})-(?P<end_year>[0-9]{4})/$", views.range_report)

include("polls.urls", namespace="author-polls")

app_name declares an app's namespace; include()'s namespace= overrides it per-instance — needed whenever the same app is included more than once.

reverse("author-polls:index") # -> "/author-polls/"

djangorouting
URL namespaces

reverse("app:view-name", kwargs={"pk": obj.pk})

reverse() resolves a URL name immediately; reverse_lazy() defers resolution — required for class attributes like success_url, evaluated at import time.

success_url = reverse_lazy("articles:list")

path("api/v1/", include("api.v1.urls"))

Version an API by URL path prefix with a separate urls.py per version; name URLs after the resource and action, not the HTTP method.

path("orders/<int:pk>/", views.OrderDetailView.as_view(), name="order-detail")

Function-Based Views

4

JsonResponse(data, safe=False) # for a non-dict payload

render() builds a template response, redirect() builds a 302, JsonResponse serializes a dict (or a list with safe=False) as JSON.

return redirect("articles:detail", pk=article.pk)

request.GET.getlist("tag")

GET/POST are QueryDicts (repeated keys via getlist()); POST never carries files — use FILES; check request.method, not `if request.POST`.

if request.method == "POST": f = request.FILES["document"]

@require_POST def delete_article(request, pk): ...

An FBV receives every HTTP method through the same function — branch on request.method, or use @require_POST/@require_http_methods for a proper 405 on mismatch.

if request.method == "POST": form = ArticleForm(request.POST)

if request.user.is_authenticated: ...

request.user is always a real user or AnonymousUser, never None — check .is_authenticated, never `if request.user:`.

request.session["cart_id"] = cart.id

Class-Based Views

5

class MyView(View): def get(self, request): ...

as_view() returns a plain function; dispatch() (inherited) routes to get()/post()/etc. by request.method — a missing method becomes a 405 automatically.

path("ping/", Ping.as_view())

def get_queryset(self): return Book.objects.filter(...)

TemplateView needs only template_name; ListView adds a queryset (override get_queryset() to filter). Always call super().get_context_data() first.

context = super().get_context_data(**kwargs) context["extra"] = "data"

pk_url_kwarg = "article_id" # DetailView lookup by non-default kwarg

DetailView/CreateView/UpdateView share get_object()/ModelForm plumbing; DeleteView splits GET (confirm) from POST (delete) — never delete inside get().

class ArticleUpdateView(UpdateView): model = Article fields = ["title", "body"]

def form_valid(self, form): ...; return super().form_valid(form)

form_valid() runs after validation passes (return super() to keep the redirect); form_invalid() runs on failure (default: re-render with errors).

def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["user"] = self.request.user return kwargs

class MyView(SomeMixin, ListView): ... # mixin first

A mixin must be listed before the base view class — Python's MRO resolves left to right. Stick to one generic-view family per CBV.

class PublisherDetailView(SingleObjectMixin, ListView): ...

Templates

4

{% extends "base.html" %} {% block content %}{{ block.super }}...{% endblock %}

{% extends %} must be the first tag; an unoverridden block falls back to the parent; {{ block.super }} adds to it rather than replacing it.

{% include "articles/_comment_list.html" with comments=article.comments only %}

mark_safe(markdown.markdown(text)) # trust the library's output, not the raw input

Autoescaping is on by default — |safe/mark_safe() must only be applied to a KNOWN trusted transformation's output, never raw user input.

{{ article.rendered_html|safe }}

djangotemplatessecurity
Autoescaping and safe strings

@register.filter def myfilter(value, arg): ...

Custom tags/filters live in templatetags/, need {% load name %} per-template; APP_DIRS searches DIRS then each app's templates/ — first match wins.

{% load blog_extras %} {{ article.body|truncate_words:30 }}

{% csrf_token %} {# inside every POST <form> #}

Never hard-code what these compute: {% csrf_token %} in every POST form, {% static %} for asset paths, {% url %} for links.

<form method="post" action="{% url 'articles:create' %}"> {% csrf_token %}

Forms

5

class ArticleForm(forms.ModelForm): class Meta: model = Article fields = [...]

ModelForm generates fields from Meta.model/fields; save() creates/updates a real instance — use commit=False to set an unexposed field first.

article = form.save(commit=False) article.author = request.user article.save()

djangoforms
ModelForm

forms.CharField(widget=forms.Textarea(attrs={"rows": 10}))

A widget is presentation only, separate from validation; initial only affects an unbound form's first display, never a bound (submitted) one.

form = ContactForm(initial={"subject": existing_value})

def clean_<field>(self): ...; return self.cleaned_data["field"]

clean_<field>() must return the cleaned value; clean() runs once for cross-field rules — use add_error("field", msg) to attach a message to a specific field.

self.add_error("subject", "Must mention help when CCing yourself.")

MyForm(request.POST, request.FILES)

disabled=True is enforced server-side (safer than HTML readonly); a file-carrying form needs request.FILES and enctype="multipart/form-data".

{% if form.is_multipart %}<form enctype="multipart/form-data">{% endif %}

inlineformset_factory(Author, Book, fields=["title"])

formset_factory is for plain forms, model/inline formset factories for model-backed ones — always render {{ formset.management_form }}.

formset = BookFormSet(request.POST, instance=author)

Django Models

3

models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

default=uuid.uuid4 (the callable, not a call) for a UUID primary key; db_index/unique are database-level; db_column renames only the column.

reference = models.CharField(max_length=20, unique=True, db_index=True)

class Meta: ordering = ["-published_at"]

Meta.ordering sets the default sort for unordered queries; an explicit .order_by() overrides it; it does NOT apply inside annotate()/aggregate() GROUP BY.

class Meta: db_table = "cms_article" verbose_name_plural = "Articles"

models.UniqueConstraint(fields=[...], condition=models.Q(...), name="...")

UniqueConstraint/CheckConstraint enforce rules at the database level; models.Index is purely for query speed — both need a migration to take effect.

models.Index(fields=["last_name", "first_name"], name="name_idx")

Django Field Types

3

models.DecimalField(max_digits=10, decimal_places=2)

DecimalField (exact) for money, never FloatField (binary, imprecise). CharField requires max_length; TextField has none.

stock = models.PositiveIntegerField(default=0)

created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True)

auto_now_add sets the value once, at creation; auto_now updates on every save() — neither applies during a bulk QuerySet.update().

Article.objects.filter(...).update(updated_at=timezone.now())

preferences = models.JSONField(default=dict)

JSONField's default must be a callable (dict, not {}); SlugField auto-indexes; ImageField needs Pillow installed.

avatar = models.ImageField(upload_to="avatars/", blank=True)

`null` vs `blank`

1

blank=True # forms may leave it empty null=True # database column may store NULL

null is database-level; blank is validation-level — independent, but usually needed together except on CharField/TextField, which should use blank=True alone.

published_at = models.DateField(null=True, blank=True)

djangoormmodels
null vs blank

Choices and Enumerations

2

class Status(models.TextChoices): PENDING = "PENDING", "Pending"

TextChoices/IntegerChoices define a fixed value set as named, importable enum members — the database still stores only the raw value.

status = models.CharField(max_length=20, choices=Status, default=Status.PENDING)

Relationships

3

ForeignKey(Model, on_delete=..., related_name=..., related_query_name=...)

ForeignKey is many-to-one; OneToOneField adds uniqueness; ManyToManyField uses a separate join table. related_name renames the reverse accessor; related_query_name renames the filter() keyword.

customer = models.ForeignKey("Customer", on_delete=models.PROTECT, related_name="orders")

<model>_set (or related_name) · ManyToManyField(through=Model)

Every relationship gets a reverse manager automatically; a through model is needed only when the link itself needs extra fields.

modelsrelationshipsmany-to-many
Reverse relations and through models

ForeignKey("self") · ManyToManyField("self", symmetrical=False)

"self" references the model being defined; ManyToManyField("self") defaults to symmetrical=True — set False for one-directional (follows).

on_delete Behavior

2

ForeignKey(Model, on_delete=models.CASCADE | PROTECT | RESTRICT | SET_NULL | SET_DEFAULT | SET(x) | DO_NOTHING)

CASCADE deletes along; PROTECT/RESTRICT block (RESTRICT allows a cascade-resolved deletion); SET_NULL/SET_DEFAULT/SET(x) replace the reference; DO_NOTHING relies on the database.

coupon = models.ForeignKey("Coupon", on_delete=models.SET_NULL, null=True)

CASCADE · PROTECT · SET_NULL — ask "what should happen?" first

CASCADE is a data-loss decision, not a neutral default — the right on_delete comes from the business rule, not from what compiles fastest.

Model Methods and Properties

3

instance.save(update_fields=[...]) / instance.delete()

save() never validates on its own — call full_clean() explicitly if needed. delete() removes the row; the Python instance survives with pk reset to None.

order.save(update_fields=["status"])

instance.full_clean() # clean_fields() → clean() → validate_unique() → validate_constraints()

clean() is your custom cross-field logic; full_clean() runs the complete four-step pipeline. Neither is called by save() automatically.

try: article.full_clean() except ValidationError as e: ...

djangoormmodelsvalidation
clean() and full_clean()

Django ORM Fundamentals

3

Model.objects.filter(...).exclude(...) # builds, does not run

QuerySets are lazy and clone (not mutate) on each chained call; evaluation (iteration, list(), count(), etc.) is what actually runs the SQL and populates the cache.

list(Entry.objects.filter(a=1).exclude(b=2)) # one query, run here

Model.objects.get(...) / .filter(...).first() / .filter(...).exists()

get() raises for zero or multiple matches — use only where that would be a bug. first()/last() return None instead. exists()/count() avoid loading full rows.

Order.objects.filter(status="PENDING").exists()

djangoormqueryset
The retrieval methods

Model.objects.update_or_create(lookup=value, defaults={...})

update()/delete() bypass save()/signals/auto_now entirely. get_or_create()/update_or_create() return (obj, created) — only kwargs outside defaults are used for the lookup.

Order.objects.filter(status="PENDING").update(status="EXPIRED", updated_at=timezone.now())

djangoormqueryset
The write methods

Query Expressions

3

F("field") + 1 / Q(a=1) | Q(b=2)

F() computes inside the database, avoiding a read-then-write race — refresh_from_db() after save() to see the real value. Q() combines lookups with &/|/~ for AND/OR/NOT.

Model.objects.filter(pk=1).update(count=F("count") + 1)

Case(When(cond, then=...), default=...) · ExpressionWrapper(expr, output_field=...)

Always include default= or unmatched rows get NULL; ExpressionWrapper declares an output type, it does not cast (that's Cast()).

Aggregation and Annotation

3

Model.objects.aggregate(x=Avg("f")) / Model.objects.annotate(x=Count("rel"))

aggregate() returns one summary dict for the whole QuerySet. annotate() adds a per-object computed value and stays chainable. Sum/Avg/Min/Max need default= to avoid None on empty results.

Author.objects.annotate(book_count=Count("book"))

Count("x", filter=Q(...), distinct=True)

filter= scopes one aggregate; distinct=True undoes join-caused row multiplication when combining Count() over different relations.

.values(field).annotate(...) — GROUP BY field

values() before annotate() groups (GROUP BY); filter() before annotate() is WHERE-like, filter() after is HAVING-like.

Advanced ORM Queries

3

Outer.objects.annotate(x=Subquery(inner.values("f")[:1])) / .filter(Exists(inner))

OuterRef bridges an inner queryset back to the outer query's field. Subquery returns a real value; Exists returns True/False and is faster for presence checks.

Post.objects.filter(~Exists(Comment.objects.filter(post=OuterRef("pk"))))

djangoormquerysetadvanced
Subquery, OuterRef, and Exists

Window(expression, partition_by=[...], order_by=..., frame=...)

Computes a value per row within a partition without collapsing rows — unlike values()+annotate() grouping.

ormwindow-functionsqueries
Window functions

Model.objects.raw(sql, params) / RawSQL(sql, params, output_field=...)

.raw() replaces a whole query; RawSQL embeds one fragment. Both need params as a real tuple/list, never string-interpolated. extra() is legacy — avoid in new code.

Order.objects.raw("SELECT * FROM orders_order WHERE total > %s", [100])

QuerySet Evaluation

1

for x in qs / list(qs) / len(qs) / bool(qs) / qs[5] / qs.count() / qs.exists()

A QuerySet evaluates on iteration, list(), len(), bool(), a single index, count(), exists(), serialization, and template rendering. A SLICE (qs[0:5]) stays lazy. count()/exists() skip the general cache.

Order.objects.filter(status="PENDING").exists() # not bool(queryset) or len(queryset)

djangoormquerysetperformance
What triggers QuerySet evaluation

N+1 Queries

2

from django.db import connection, reset_queries

N+1 is 1 query plus N more from a related-field access inside a loop — invisible in the code. Measure with connection.queries before assuming a fix worked.

reset_queries(); list(qs); print(len(connection.queries))

1

Model.objects.select_related("fk_field__nested_fk_field")

Fixes N+1 for forward FK and forward/reverse OneToOne via a single SQL JOIN — never a reverse FK or M2M (use prefetch_related() for those). Chains multiple levels with __.

Book.objects.select_related("author__hometown")

djangoormperformance
select_related()
2

Model.objects.prefetch_related("m2m_field") / .prefetch_related("reverse_fk_set")

Runs a separate query for a "many" relationship and joins results in Python — the fix select_related() can't provide. .filter() on the related manager afterward bypasses the cache.

Restaurant.objects.prefetch_related("pizzas")

Prefetch("relation", queryset=..., to_attr="name")

Needed for a filtered/ordered prefetch, or when the same relation is prefetched twice — to_attr avoids a silent overwrite.

Query Projection

2

Model.objects.values("a", "b") / .values_list("a", flat=True)

values() → dicts; values_list() → tuples (flat=True for single-field bare values, named=True for namedtuples). Both are real SQL projections, skipping full model-instance construction.

Order.objects.filter(status="PENDING").values_list("id", flat=True)

djangoormperformance
values() and values_list()

Model.objects.only("a", "b") / Model.objects.defer("a", "b")

Real model instances with a limited immediate field set. Touching a deferred field triggers a separate query. only() + select_related() must include the joined relation's field(s).

Book.objects.select_related("author").only("title", "author__name")

djangoormperformance
only() and defer()

PostgreSQL with Django

6

CREATE INDEX idx ON table USING GIN|GiST|BRIN (column);

B-tree (default) for equality/range; GIN for array/JSONB/full-text containment; GiST for geometric/nearest-neighbor; BRIN for huge, insertion-ordered tables.

CREATE INDEX idx_products_attributes ON products USING GIN (attributes);

djangopostgresqlperformance
Index types

EXPLAIN ANALYZE SELECT ...;

EXPLAIN shows estimates only; EXPLAIN ANALYZE actually runs the query and adds real timing. A Seq Scan with a large "Rows Removed by Filter" signals a missing index.

BEGIN; EXPLAIN ANALYZE UPDATE ...; ROLLBACK; -- for a modifying statement

SET TRANSACTION ISOLATION LEVEL READ COMMITTED | REPEATABLE READ | SERIALIZABLE;

Read Committed (default) re-snapshots per statement; Repeatable Read snapshots once per transaction; Serializable adds full conflict detection — both stricter levels require retry-on-failure.

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

djangopostgresqltransactions
Transactions and isolation levels

always lock multiple rows/tables in the SAME order across every code path

A deadlock is a circular wait — PostgreSQL aborts one transaction unpredictably to break it. Prevention (consistent lock order) beats relying on detection.

Account.objects.select_for_update().filter(id__in=sorted([id1, id2])).order_by("id")

djangopostgresqlconcurrency
Locks and deadlocks

VACUUM ANALYZE table_name;

VACUUM reclaims dead-tuple space (non-blocking); VACUUM FULL shrinks the table but blocks everything. autovacuum runs both automatically. ANALYZE refreshes planner statistics.

VACUUM ANALYZE orders; -- routine, safe alongside normal traffic

djangopostgresqlperformance
VACUUM and ANALYZE

PgBouncer (not max_connections) · read-your-own-write → primary · partitioning

A connection pooler is the standard fix for too many app connections; async replication can lag, so route read-your-own-write to the primary.

postgresqlconnection-poolingreplication
Connection limits, replicas, and partitioning

Database Constraints

2

models.Index(Lower("field"), name="required_name")

Indexes an expression's result, not the raw column — only accelerates a query using that exact same expression. name= is required.

models.Index(Lower("email"), name="email_lower_idx") # backs __iexact lookups

djangoormmodelsperformance
Functional indexes

field = models.SomeField(db_default=Now())

A database-computed default, applying to every write path (not just Django's ORM). Distinct from default=; an unsaved instance reads a placeholder until refresh_from_db().

created = models.DateTimeField(db_default=Now())

Transactions

3

@transaction.atomic / with transaction.atomic():

Groups statements into one real transaction (all-or-nothing). A nested atomic() block is a savepoint — its rollback stays scoped to it, but the outermost block is the true commit boundary.

with transaction.atomic(): try: with transaction.atomic(): optional_step() except SomeError: pass

catch OUTSIDE atomic() — retry serialization failures, never a genuine IntegrityError

Catching a DB error inside atomic() and continuing raises TransactionManagementError — the transaction is broken from that point on.

transaction.on_commit(lambda: side_effect())

Defers a side effect until the transaction genuinely commits — never runs on rollback. durable=True forces a block to be the outermost transaction, raising if nested.

transaction.on_commit(lambda: send_confirmation_email(order.id))

select_for_update()

2

Model.objects.select_for_update(nowait=False, skip_locked=False, of=()).get(...)

Locks matched rows for the duration of the enclosing transaction — must be inside atomic(). nowait fails fast; skip_locked silently skips locked rows.

with transaction.atomic(): item = InventoryItem.objects.select_for_update().get(pk=item_id)

djangoormconcurrencytransactions
Row-level locking with select_for_update()

select_for_update() — read-then-decide-then-write; F() for unconditional changes

A simple unconditional change is cheaper with F(); test locking with TransactionTestCase, not TestCase.

Concurrency and Race Conditions

2

read + decide + write = a race window; no read (F()) or a lock (select_for_update()) closes it

A race condition is a silent lost update, not a crash. Match the fix to the actual need: F() (no decision needed), select_for_update() (a genuine decision), or a constraint (reject the bad outcome after the fact).

Account.objects.filter(pk=1).update(balance=F("balance") - amount) # no read step, no race

Order.objects.filter(pk=id, status="PENDING").update(status="PAID")

A conditional update folds the precondition into the WHERE clause — always check the returned row count before a once-only side effect.

Migrations

3

python manage.py makemigrations → python manage.py migrate

makemigrations diffs models.py into a migration file (no DB touched); migrate applies migration files to the real database. A conflict (two migrations on the same dependency) needs --merge.

python manage.py makemigrations --merge # resolves two conflicting branch migrations

RunPython(forward, reverse_code=reverse) — omit it, block every rollback

Built-in schema operations reverse automatically; RunPython/RunSQL need an explicit reverse or they become a one-way door.

Model = apps.get_model("app_label", "ModelName") # inside RunPython, always

apps.get_model() returns the HISTORICAL model at this migration's point in history — never import the real, current model inside RunPython.

migrations.RunPython(backfill_full_name) # backfill_full_name uses apps.get_model() internally

Safe Database Migrations

2

add(null=True) → RunPython backfill → alter(null=False)

Adding a non-nullable field to a table with existing rows needs three migrations, never one. atomic=False + small batches avoids one long lock on a large backfill.

class Migration(migrations.Migration): atomic = False operations = [migrations.RunPython(backfill_in_batches)]

expand → dual-write → backfill → switch over → contract

A rolling deployment has a real window where old and new code coexist; a direct rename/removal breaks whichever side is still old.

Django Managers

2

class MyManager(models.Manager): def get_queryset(self): ... def a_shortcut(self): ...

A manager method is a named, opt-in query shortcut; get_queryset() overrides the DEFAULT starting point for every query through that manager. Multiple independent managers can coexist on one model.

Book.objects.all() # unfiltered Book.dahl_objects.all() # pre-filtered via get_queryset()

Custom QuerySets

2

class MyQuerySet(models.QuerySet): def a_filter(self): return self.filter(...)

Custom QuerySet methods must return self.filter(...) to stay chainable. The manager's get_queryset() must return an instance of the custom class, or the methods are unreachable from Model.objects.

Order.objects.completed().recent() # chains only if get_queryset() returns OrderQuerySet

objects = Manager.from_queryset(MyQuerySet)()

Generates a manager whose get_queryset() returns MyQuerySet — needed for manager-only methods plus chainable QuerySet ones. Don't forget the trailing ().

Django Admin

3

class MyAdmin(admin.ModelAdmin): list_display = [...] search_fields = ["^field", "=field", "@field"]

list_display shows fields/related lookups/callables (never a raw ManyToManyField). search_fields defaults to icontains; ^/=/@ switch to istartswith/iexact/full-text.

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

get_queryset() (sees) + has_*_permission() (does) — both required

Inlines edit related objects on the parent page; a custom action runs against a bulk-selected QuerySet, not one row at a time.

list_select_related = [...] / get_queryset(request).annotate(x=Count(...))

A list_display method touching a relationship is N+1, exactly like anywhere else. list_select_related fixes a single-valued FK/OneToOne; annotate() in get_queryset() fixes a count/aggregate over a "many" relationship.

class MyAdmin(admin.ModelAdmin): list_select_related = ["author"] def get_queryset(self, request): return super().get_queryset(request).annotate(n=Count("comments"))

Authentication

3

settings.AUTH_USER_MODEL = "app.User" get_user_model()

The indirection every Django internal uses to reference "the" user model — never a direct import.

class Order(models.Model): owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

user = authenticate(request, ...); login(request, user)

authenticate() checks credentials and never touches the session; login() is the separate step that establishes it.

Authorization

2

user.has_perm("app_label.codename") Meta.permissions = [("codename", "description")]

Model-level and custom permissions, checked directly or via any Group a user belongs to.

editors.permissions.add(Permission.objects.get(codename="can_publish_article"))

Sessions and Cookies

2

SESSION_ENGINE = "django.contrib.sessions.backends.<db|cache|cached_db|signed_cookies>"

How and where session data is actually stored between requests.

request.session.flush() # invalidate + rotate the session key

Middleware

3

def __call__(self, request): ... # request processing response = self.get_response(request) ... # response processing return response

MIDDLEWARE order = request-processing order; reversed for response processing. Return early to short-circuit.

if not authorized: return HttpResponse(status=403) # short-circuits, view never runs

Signals

2

@receiver(post_save, sender=Model) def fn(sender, instance, created, **kwargs): ...

Connect a receiver to a model signal — created distinguishes insert vs update; register inside AppConfig.ready().

@receiver(m2m_changed, sender=Article.tags.through) def fn(sender, instance, action, pk_set, **kwargs): ...

Transactions + Signals

1

Static Files

2

Media Files and Uploads

3

Django REST Framework — Must Be Strong for API Roles

3

class MySerializer(serializers.ModelSerializer): class Meta: model = Model fields = [...]

ModelSerializer auto-generates fields/validators from the model, mirroring ModelForm.

serializer.is_valid(raise_exception=True) serializer.validated_data

DRF Views

3

class MyView(generics.ListCreateAPIView): queryset = ... serializer_class = ...

APIView (hand-written) → a mixin (one behavior) → a generic view (fully pre-wired) — a spectrum of how much is generated.

class ArticleList(generics.ListAPIView): def get_queryset(self): return Article.objects.filter(status="published")

router.register(r"articles", ArticleViewSet) — @action(detail=True) uses self.get_object()

A ViewSet organizes code by resource, not URL; a Router auto-generates URLs — set basename explicitly when queryset isn't a class attribute.

DRF Authentication

3

authentication_classes = [JWTAuthentication, SessionAuthentication]

Per-view list of authentication schemes; DRF tries each in order and stops at the first that returns a user.

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

DRF Permissions

3

DRF Filtering, Search, and Ordering

3

filter_backends = [DjangoFilterBackend]; filterset_class = OrderFilter

Declarative, closed-list filtering applied between get_queryset() and pagination.

GET /orders/?status=paid&min_total=100

DRF Pagination

3

DRF Throttling and Rate Limiting

3

throttle_classes = [ScopedRateThrottle]; throttle_scope = "login"

Per-endpoint rate limiting; the rate is looked up by scope name in DEFAULT_THROTTLE_RATES.

"5/min" — only the first character of the period is read

SimpleRateThrottle + get_cache_key() — shared Redis, not LocMemCache

LocMemCache gives one counter per process, so workers × instances multiplies your limit — never bill against a throttle, its counters are approximate.

edge (volumetric) → DRF throttle (pacing, approximate) → quota (exact, transactional)

A throttle shapes traffic and may be approximate; a quota is accounting and must be exact — raise Throttled for a 429 + Retry-After.

drfthrottlingrate-limiting
Abuse prevention and quotas

API Design

5

GET|POST /orders/ · GET|PATCH|DELETE /orders/{id}/

The resource-oriented shape a DRF router generates: noun in the path, verb in the method.

POST /orders/57/refunds/ — an action modelled as a sub-resource

safe: GET HEAD OPTIONS · idempotent: + PUT DELETE

The two HTTP method promises that caches, proxies and client libraries act on without asking.

DELETE twice → 204 both times, never 404 on the retry

Sunset: Wed, 01 Jul 2026 00:00:00 GMT

RFC 8594 header naming the date after which an endpoint may stop responding — machine-readable deprecation.

Deprecation: true · Link: </docs/migrating-to-v2>; rel="deprecation"

API Idempotency

2

API Error Handling

4

400 fix input · 401 authenticate · 403 refused · 404 absent · 409 state · 429 wait

The 4xx codes, chosen by the remedy each one implies for the client.

raise Throttled(wait=42) → 429 with Retry-After: 42

500 (yours) · 502/503/504 (proxy) — statement_timeout < Gunicorn --timeout < proxy timeout

A 500 has a Django traceback; 502/503/504 usually mean the proxy answered because your app died, was unavailable, or was slow.

error-handlinghttp-status-codes
500, and the 502 / 503 / 504 family

REST_FRAMEWORK = {"EXCEPTION_HANDLER": "common.exceptions.handler"}

One function that renders every DRF exception; call the default first to keep status codes and required headers.

exception_handler(exc, context) returning None == an unrecognised exception, on its way to a 500

OpenAPI and API Documentation

3

Django Security

4

@csrf_exempt · |safe · mark_safe() · f-string in .raw()

The four lines that disable Django's built-in defences — what to grep for in a security review.

Order.objects.raw("... WHERE reference = %s", [reference]) — params, never an f-string

ALLOWED_HOSTS + SECURE_SSL_REDIRECT + HSTS + Secure/HttpOnly/SameSite

ALLOWED_HOSTS stops forged Host headers; SSL_REDIRECT covers request 2+, HSTS covers request 1 — ramp max-age, run check --deploy.

url_has_allowed_host_and_scheme(url, allowed_hosts, require_https)

Django's own open-redirect check — the one LoginView uses on its `next` parameter.

SSRF: resolve the hostname first, then allow-list the IP; 169.254.169.254 is cloud metadata

python manage.py check --deploy --settings=config.settings.production

Audits the production settings for missing security configuration; each gap gets an id like security.W004.

SECRET_KEY_FALLBACKS = [old_key] — rotate the signing key without logging everyone out

securitysecretsdependenciesdeployment
Dependencies, secrets, and check --deploy

Caching

3

KEY_PREFIX:VERSION:key — timeout=None (never expires), timeout=0 (no cache)

Redis persists, Memcached forgets, LocMemCache is per-process; bumping VERSION is the cheapest bulk invalidation.

Redis for Django

3

ZADD key score member · ZRANGEBYSCORE key min max

Sorted sets: a float score per member, kept ordered — leaderboards, sliding windows, and delayed queues.

ZREMRANGEBYSCORE rl:42 0 (now-60) — trim a rate-limit window by time

Async Django

3

ASGI vs WSGI

2

WSGI: application(environ, start_response) · ASGI: await application(scope, receive, send)

The two calling conventions; ASGI's message channels are what make WebSockets and streaming expressible.

gunicorn config.asgi:application -k uvicorn.workers.UvicornWorker

Background Jobs

2

transaction.on_commit(lambda: task.delay(obj.pk))

The only safe way to enqueue from inside a transaction: fires after COMMIT, cancelled by a rollback.

Return 202 with a job id and a poll URL rather than blocking the request.

background-jobscelerytransactions
What belongs in a background job

Celery

5

@shared_task · task.delay(*args) · celery -A config worker -l info

Register with the current app, publish a message, and consume it in a separate process.

app.autodiscover_tasks() imports <app>/tasks.py only — nothing else is registered.

task_ignore_result=True — separate Redis DB, visibility_timeout > slowest task

The broker is required and carries the message; the result backend is optional and stores the return value.

@shared_task(autoretry_for=(Exc,), retry_backoff=True, max_retries=5)

Declarative retries with exponential backoff and jitter; list only exceptions a retry could resolve.

raise self.retry(countdown=int(response.headers["Retry-After"])) — honour their advice

CELERY_TASK_ROUTES = {"media.tasks.*": {"queue": "slow"}}

Route by task-name pattern, then start a worker per queue with -Q, its own -c and its own time limits.

soft_time_limit raises inside the task; time_limit kills the child and skips finally.

@shared_task(acks_late=True) + unique-constraint claim in the same transaction

acks_late requires an idempotent task; alert on queue depth, oldest-message age, and failure rate — Flower is a debugger, not the alert.

Queue Semantics

2

at-most-once: ack first · at-least-once: ack after · exactly-once: effect only

The two real guarantees and the one that is not on offer; idempotency is what bridges the gap.

filter(sku=sku, version__lt=version).update(version=version) — reordering made harmless

Django Testing

4

SimpleTestCase < TestCase < TransactionTestCase

No database, rolled-back transaction, or real commits plus truncation — in increasing order of cost.

with self.captureOnCommitCallbacks(execute=True): … — test on_commit without the slow class

@pytest.mark.parametrize + pytest-asyncio + factory defaults

parametrize turns near-identical tests into one body plus a table; async tests need pytest-asyncio, and the ORM still raises SynchronousOnlyOperation.

Mocking

3

stub (canned) · fake (working, simplified) · spy (wraps + records) · mock (expectations)

Reach for the least specific double that can observe what you care about — fakes/stubs survive refactors, a strict mock asserts interaction.

Django API Testing

3

APIClient().force_authenticate(user) · .credentials(HTTP_AUTHORIZATION=…)

The first bypasses authentication (authorization tests); the second exercises it (authentication tests).

Always test the list route separately — has_object_permission() never runs for it.

Database Testing

3

TestCase (rollback) · TransactionTestCase (truncate) · @pytest.mark.django_db(transaction=True)

The test database is `test_<NAME>`; the class decides whether a test may really commit.

Try captureOnCommitCallbacks(execute=True) before moving a test to TransactionTestCase.

pytest.raises(IntegrityError, match="<constraint name>") + inner transaction.atomic()

Constraint tests bypass application validation; the inner atomic() keeps the abort local.

A real race needs django_db(transaction=True), threads, a Barrier, and connection.close().

django_assert_num_queries(n) · django_assert_max_num_queries(n) · assertNumQueries(n, func)

Turns a performance property into a deterministic assertion that runs in milliseconds.

Re-run at 10x rows with the same budget — that is the N+1 check, not the raw count.

Contract and Integration Testing

3

responses.RequestsMock(assert_all_requests_are_fired=True) · @pytest.mark.integration

Stub the HTTP boundary for every branch; keep one tagged sandbox test for the shape.

Never patch your own client class — that deletes the URL, headers and timeout from coverage.

@responses.activate(registry=OrderedRegistry) · body=ReadTimeout() · timeout=(3.05, 10)

Ordered stubs let attempt 1 fail and attempt 2 succeed, so a retry policy becomes testable.

Assert the set of Idempotency-Key headers has size 1 — the count alone proves nothing.

client.post(url, data=raw_bytes, content_type="application/json", HTTP_X_SIGNATURE=…)

Four webhook tests: forged signature, valid event, duplicate delivery, out-of-order pair.

Verify over request.body with hmac.compare_digest, then parse — never the other way round.

Performance Fundamentals

3

cost ≈ round trips × latency, not instructions × cycles

Big-O in a web request is measured in queries and network hops, not Python statements.

O(n) queries is the bug; O(n) in-memory work over a page of rows is not.

EXPLAIN (database time) · .values() (instance cost) · hit/miss counters (cache)

Three separate costs behind one "slow endpoint", each with a different fix.

Fast SQL plus a slow endpoint means the serializer — profile the view, not the query.

Django Performance Debugging

4

total_ms = middleware_ms + view_ms; view_ms = db_ms + external_ms + cpu_ms

Split the duration before reading any code — the layers look identical from one number.

A floor under every endpoint, including /healthz, means middleware, not any view.

performancedebuggingmiddleware
Locating the time in a slow request

qs.explain(analyze=True) · pg_blocking_pids() · assertNumQueries at two data sizes

Three database symptoms with three signatures; only one of them is an index problem.

Fast alone, slow under load, healthy plan = contention. Look for a long transaction.

tracemalloc.take_snapshot().compare_to(before, "lineno")

Names the lines that allocated between two points — the tool for "why did the process die".

Fix follows the naming: .values() removes instances, .iterator() removes all-at-once.

py-spy dump --pid · cProfile -s cumtime · Debug Toolbar (local only) · APM

Each tool answers one question; choose by where you are and how wide the question is.

A cache span slower than the database span means the cache has become a cost.

Pagination and Large Result Sets

3

order_by("-placed_at", "-id") · qs[a:b] → LIMIT/OFFSET · cursor → WHERE (key, id) < (…)

Offset boundaries are counts that move and deepen; cursor boundaries are values that do not.

Fetch limit+1 rows to answer "has next" without a COUNT(*).

qs.iterator(chunk_size=2000) + StreamingHttpResponse(generator)

Chunked reads plus a streamed body — peak memory becomes a constant you chose.

With prefetch_related, chunk_size is mandatory or the prefetch is silently skipped.

POST → 202 {job_id} · worker → object storage · GET → signed URL

The export shape that survives dropped connections, deploys and retries.

A streamed response commits its status code with the first byte — failures arrive inside a 200.

exportsbackground-jobsstorage
Large export jobs

Large Data Operations

3

bulk_create(objs, batch_size, update_conflicts, update_fields, unique_fields) · bulk_update(objs, fields, batch_size)

Collapse thousands of statements into a handful — at the cost of save() and every signal.

Reproduce the skipped save() work in the loop that builds the objects, also in bulk.

qs.update(f=F("f") + 1) · loop { atomic { qs.filter(pk__in=batch).delete() } }

Change rows without loading them; delete them in committed batches, not one statement.

The transaction goes inside the delete loop — outside it keeps every drawback of one huge DELETE.

apps.get_model() · atomic = False · RunPython(..., elidable=True) · resume from last_pk

Long backfills belong in jobs; when they must be migrations, make them chunked and re-runnable.

Filter on what is not done yet — that single clause is what makes a restart cheap.

Database Connection Management

3

CONN_MAX_AGE (0 | seconds | None) + CONN_HEALTH_CHECKS = True

Trades per-request connection setup for connections held per worker process.

Turning it on changes the count from "concurrent requests" to "worker processes".

hosts × processes × concurrency + celery + ad-hoc ≤ max_connections − reserved

Connections scale with processes, not traffic — and the ceiling refuses rather than degrades.

statement_timeout, idle_in_transaction_session_timeout and lock_timeout bound how long a slot is held.

OPTIONS={"pool": True} (per process) · PgBouncer transaction mode (shared)

One amortises connection setup; only the shared one reduces the fleet-wide count.

Transaction pooling requires DISABLE_SERVER_SIDE_CURSORS = True.

Read Replicas

2

db_for_read · db_for_write · allow_relation · allow_migrate → DATABASE_ROUTERS

Four independent questions asked per query; `None` means "no opinion", not "use default".

allow_migrate should return db == "primary" — replicas are maintained by replication.

Model.objects.using("primary") · sticky window after any write · monitor lag

Two read categories may never be routed: your own recent write, and anything a write depends on.

Lag is also the size of the data loss on failover, not only added latency.

Observability

4

metrics (aggregate, low cardinality) · traces (cross-service, sampled) · logs (per-event detail)

Three questions, three cost models — joined by a shared request_id.

Label with resolver_match.route, never request.path — the path contains ids.

X-Request-ID → ContextVar → logging.Filter → every record, task and outbound call

One id per unit of work, injected automatically rather than passed by hand.

Keep exception messages stable; variable data goes in context, or grouping breaks.

latency (p50/p95/p99) · error ratio · saturation · db time+count · hit rate · depth+age

Six signals; saturation leads, latency lags, and queue age is invisible to request metrics.

Depth alone cannot tell a draining backlog from a stuck one — oldest-job age can.

observabilitymetricsmonitoring
The signals worth measuring

<symptom> <threshold> for <duration> + runbook link

Alerts interrupt people; dashboards are read on purpose. Never promote one into the other.

Sentry (errors) · Prometheus (series) · Grafana (dashboards) · OpenTelemetry (emission).

observabilityalertingtooling
Alerts, dashboards, and the ecosystem

Structured Logging

3

loggers → filters → handlers (level + filters) → formatters

Four components, four filtering gates, and a name hierarchy records climb by default.

propagate: False on any logger you attach handlers to, or every line is emitted twice.

log.info("stable message", extra={"order_id": …, "tenant_id": …})

The message groups; the fields filter. Interpolation destroys the first half.

Formatters should emit an allow-list of extra keys, and use default=str so they cannot raise.

allow-list formatter + RedactingFilter + @sensitive_variables / @sensitive_post_parameters

Three layers, because call-site discipline fails exactly where nobody was thinking.

A block-list fails open — card_number is caught, cardNumber and pan are not.

loggingsecurityprivacy
Keeping secrets out of logs

Email Systems

3

EmailMultiAlternatives(subject, body, to=[…]).attach_alternative(html, "text/html")

The message is plain text; HTML is an alternative attached to it.

EMAIL_BACKEND swaps the transport — locmem fills mail.outbox for tests.

transaction.on_commit(lambda: send_receipt.delay(delivery.id))

Enqueue after commit, pass an id, retry only transient failures.

retry_backoff + retry_jitter, capped, with a dead-letter queue at the end.

hard bounce (5xx) → suppress · soft bounce (4xx) → retry

"Sent" means your relay accepted it; delivery status arrives later, by webhook.

Rank the states so an out-of-order webhook cannot move a delivery backwards.

Internationalization and Time Zones

3

timezone.now() → store UTC · timezone.localtime(v) → show local

An instant is universal; a wall-clock reading is not, and repeats once a year.

TruncDate("starts_at", tzinfo=…) — "per day" is a local question, not a UTC one.

Management Commands

4

<app>/management/commands/<name>.py → class Command(BaseCommand)

add_arguments declares the interface; handle receives it as **options.

--dry-run reaches handle as options["dry_run"] — dashes become underscores.

raise CommandError(msg, returncode=2) · self.stdout.write(self.style.SUCCESS(…))

The exit code is the contract; printing "failed" and returning exits 0.

Results on stdout, progress on stderr — so `cmd > out.txt` stays clean.

filter(needs_work) + atomic() per batch + set_rollback(True) unless --apply

Idempotent by construction: the work set shrinks as work commits.

Never OFFSET a queryset you are modifying — fixed rows leave it and the offset skips.

management-commandsbackfilltransactions
Dry-run, batching, and being safe to run twice

reconcile (read) → backfill (write) → reconcile again (verify)

Six jobs, sorted by what happens when the command is wrong.

Cleanup: report the CASCADE before deleting — one row can be a subtree.

File Exports and Imports

3

StreamingHttpResponse((writer.writerow(r) for r in rows), content_type="text/csv")

An iterator body: flat memory, immediate first byte, no `content` attribute.

Pair it with .iterator() — streaming a materialised list saves nothing.

Multi-Tenancy

3

claim (subdomain/header) → verify against Membership → request.tenant

Three isolation models on one dial: cheap and dense, or isolated and expensive.

404 on an unresolvable tenant — a 403 tells an attacker the customer exists.

objects = TenantManager() · base_manager_name = "all_objects"

The manager covers direct queries only — related access uses the base manager.

Scope at lookup, tenant in every cache key, ContextVar not a global.

config(tenant, key) → tenant override → plan default → system default

Limits, configuration and audit all become per-tenant data, not settings.

Audit needs tenant + actor + impersonated_by, or the log misattributes support actions.

Auditing

3

created_at/updated_at (auto_now*) + created_by/updated_by (nullable)

The cheapest auditing — and it stops at the most recent change.

update() skips save(), so set updated_at and updated_by in the update() call.

app log (days) · audit log (years, append-only) · domain events (the product)

Three stores with three audiences — one table serves none of them well.

update() skips save(), auto_now and signals — so it skips signal-based auditing too.

Soft Delete

3

deleted_at (null=live) · objects=LiveManager · all_objects · base_manager_name

One flag, two managers, and two delete() overrides — not one.

QuerySet.delete() never calls Model.delete(): override both or bulk deletes are real.

UniqueConstraint(condition=Q(deleted_at__isnull=True)) · .alive() at traversal

Four surfaces break: uniqueness, relations, admin, reporting.

Reporting is the silent one — no error, just totals that include deleted rows.

delete → batch id · restore → same batch + constraint re-check · purge → by age

A holding state needs two exits: restore, and eventual real deletion.

Do not add soft delete because it seems safer — that is the rule's actual target.

State Machines and Workflows

3

Payment and Webhook Workflows

4

raw = request.body → timestamp window → compare_digest → json.loads → 200

Verify the transmitted bytes, then parse. Never the other way round.

json.dumps(json.loads(x)) != x — a re-serialised payload can never verify.

stale non-terminal payments → fetch from provider → transition_to(source="provider")

Webhooks are the fast path; reconciliation is what makes it safe.

A timeout is ambiguous — retry with the same idempotency key, never blindly.

Event-Driven Architecture

3

Reliability Patterns

3

timeout=(connect, read) · sleep = random(0, base * 2**attempt)

Bound the wait, retry only transient failures, back off and jitter.

requests.post(url, timeout=(3.05, 10), headers={"Idempotency-Key": key})

closed → open → half-open → closed

Breaker bounds time on a bad dependency; bulkhead bounds resources; rate limit bounds arrivals.

Breaker state belongs in Redis, never in a module-level counter.

reliabilitycircuit-breakerbulkhead
Circuit breakers, bulkheads and rate limits

degrade → fall back → dead-letter · liveness ≠ readiness

Every dependency needs a decided behaviour when it is unavailable.

Liveness does no I/O; readiness fails first during a drain.

reliabilitydegradationhealth-checks
Degrade, fall back, dead-letter, health-check

Linux for Django Developers

5

systemd → gunicorn master → workers · kill -TERM / -HUP <master pid>

Processes own memory and connections; signals address the master.

SIGKILL cannot be caught — it loses in-flight requests.

r=4 w=2 x=1 · owner:group:other · chmod 600 .env

The user a process runs as is the whole of its file access.

On a directory, x means traverse — 644 there denies everything inside.

grep lines · awk fields · sed transform · curl -i · ssh host "cmd"

Read what happened on the box, then reach the next one.

On JSON logs, parse — grep matches inside values.

systemctl reload|enable --now · journalctl -u X -f · absolute paths in cron

What starts the service, where its logs go, and how a schedule differs.

TimeoutStopSec must exceed the app graceful timeout.

Networking

4

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

TLS ends at the proxy; Django learns the scheme from a header it must be able to trust.

nginx: proxy_set_header X-Forwarded-Proto $scheme; (overwrite, never append)

EventSource + text/event-stream · WebSocket + Upgrade

One-way and self-healing, or two-way and hand-rolled.

The blank line after data: is what dispatches an SSE event.

Docker for Django

2

Gunicorn, Uvicorn, and Deployment Workers

3

gunicorn config.wsgi -k sync|gthread · config.asgi -k uvicorn_worker.UvicornWorker

Supervision from gunicorn; concurrency from the worker class.

--threads > 1 on sync silently becomes gthread.

workers = min(CPU, memory ÷ RSS, DB budget ÷ hosts, 4–12)

Four ceilings; the lowest wins, and one of them is shared.

threads only apply to gthread and never add CPU throughput.

deploymentcapacitygunicorn
How many workers, how many threads

CI/CD

3

lint → types → unit → integration → scan → build → deploy → migrate → smoke

Cheapest-first gates, then act on the artefact.

makemigrations --check --dry-run · check --deploy on production settings.

Deployment Strategies

4

rolling (ramp) · blue/green (step) · canary (hold, then step)

One variable — traffic share over time — decides cost and blast radius.

Two live versions, one database: the schema must fit both.

deploymentcanaryrelease
Rolling, blue/green and canary

flags.enabled(key, user=…, default=False)

Ship dark, release separately, and undo without a deploy.

Bucket by a stable hash — random() makes the feature flicker.

deploymentfeature-flagsrelease
Feature flags: deploying is not releasing

Safe Django Upgrades

2

Architecture Styles

3

boundary in nothing · in code · in the network

Distribution relocates a boundary; it does not create one.

A modular monolith keeps atomic() across modules. A split does not.

architecturemonolithmicroservices
Monolith, modular monolith, microservices

delivery → application → domain · ports ← adapters

One direction of dependency; purity is optional and expensive in Django.

Declare a Protocol for the vendor; let tests pass a fake.

architecturelayeringtesting
Layered, clean and hexagonal

Service Layer

2