Django is a Python web framework that ships with the things most projects end up writing anyway: an ORM with migrations, an admin interface, authentication, forms, a template engine, caching, internationalisation, and security defaults that are on rather than opt-in. The tagline โ "the web framework for perfectionists with deadlines" โ is really about that: fewer decisions standing between you and the first working feature.
The request, end to end
Nothing in the flow is hidden in configuration files:
- gunicorn (WSGI) or uvicorn (ASGI) hands the request to Django.
- Middleware runs top to bottom โ sessions, CSRF, authentication, security headers.
- The URLconf maps the path to a view function.
- The view queries the ORM and renders a template, or returns JSON.
- Middleware runs in reverse on the way out.
If you can read Python, you can trace a request. That is the design goal.
What ships in the box
ORM and migrations. Models are Python classes; makemigrations diffs them into migration files, migrate applies them. QuerySets are lazy, chainable, and parameterised โ SQL injection isn't something you have to remember to prevent.
class Post(models.Model):
title = models.CharField(max_length=255)
published_at = models.DateTimeField(default=timezone.now)
recent = Post.objects.filter(published_at__lte=timezone.now())[:10]
Relations cover foreign keys, many-to-many and one-to-one, with select_related and prefetch_related for the N+1 problem.
The admin. One line per model and you have a CRUD interface with search, filters, date drill-down, permissions, and a change log. It replaces a surprising amount of internal tooling. This blog is written in it โ Markdown in, rendered HTML out.
Authentication and permissions. A swappable user model, password hashing and validators, sessions, groups, per-model permissions, and login_required decorators. Social login is third-party (django-allauth), which is the pattern throughout Django: the core is opinionated, the edges are replaceable.
Forms and validation. Form and ModelForm handle rendering, binding, cleaning and CSRF in one place, server-side. Validation lives next to the model rather than in a browser.
Templates. Django Template Language with inheritance and autoescaping by default. Django 6.0 added template partials: {% partialdef %} and {% partial %} define and render named fragments, and template.html#partial_name loads just that fragment โ which is exactly what you want for HTMX responses without splitting every component into its own file.
Security defaults. CSRF protection on unsafe methods, HTML autoescaping, parameterised queries, clickjacking middleware, and settings for secure cookies, SSL redirect and HSTS. Django 6.0 adds Content Security Policy support via ContentSecurityPolicyMiddleware and the SECURE_CSP setting.
Sessions, caching, i18n. Pluggable session backends (database, cache, signed cookies), a cache framework with per-view and template-fragment caching, translation tooling, and timezone-aware datetimes.
Static and media files. staticfiles with collectstatic, storage backends, and hashed manifest filenames โ so you can serve assets with far-future cache headers. WhiteNoise handles compression and hashing without a separate web server.
Testing. TestCase with transactional rollback, a test client, fixtures, LiveServerTestCase, and assertions like assertNumQueries that make performance regressions visible.
Feeds and sitemaps. contrib.syndication and contrib.sitemaps produce RSS/Atom and XML sitemaps in a few lines. This blog has both.
Management commands. Custom manage.py subcommands for one-off jobs and cron tasks.
Background tasks (new in 6.0). django.tasks gives you a @task decorator with priority, queue name and backend selection. The default backend runs tasks inline, so it works in development without infrastructure; a real worker backend is still a decision you make.
Async. Async views, ASGI, and async ORM methods coexist with synchronous code. You can adopt it incrementally instead of rewriting.
GeoDjango. Spatial fields, lookups and GIS functions, if your data has coordinates.
What is not in the box
Worth knowing, because the gaps are where the "batteries included" reputation oversells:
- WebSockets / realtime โ that's Channels, a separate package.
- A durable task queue โ Django 6 defines the interface; the worker is yours to choose.
- Full-text search โ database
icontainsat one end, Postgres full-text or an external engine at the other. - A CMS or page builder โ Wagtail exists for that.
- A frontend framework โ bring your own. HTMX and Alpine pair well with server-rendered templates.
- Deployment โ gunicorn or uvicorn, a proxy, a container. Django gives you the app, not the ops.
When Django fits
It fits when your problem is structured data plus users plus forms plus admin: content sites, internal tools, SaaS backends, anything where the data model is the hard part and you want conventions that scale to a team.
It fits less well for a single JSON endpoint (FastAPI is leaner), realtime-first systems, or tiny scripts where the framework is more code than the program.
This site as a worked example
Four apps: home (static portfolio), blog (models, admin authoring, Markdown rendering, feed, sitemap), recipes (relational modelling with sections, ingredients and steps, plus HTMX search), and users (custom user model). Gunicorn serves WSGI, WhiteNoise serves hashed static files, SQLite runs in WAL mode, and the whole thing deploys with one script.
Starting in five commands
uv init && uv add django
uv run django-admin startproject config .
uv run python manage.py startapp blog
uv run python manage.py migrate
uv run python manage.py runserver
The short version
Django optimises for finishing. It hands you a coherent default stack โ ORM, admin, auth, templates, security โ and an escape hatch for every part you'd rather replace. If the problem looks like "structured data, users, forms, admin", it's the shortest path there. If it looks like "one fast endpoint" or "realtime", something smaller or more specialised will be less work.