Python Framework

Django

Django is the batteries-included Python web framework. Models, migrations, an ORM, templates, auth and a production admin ship together, so you describe your data and Django handles the rest.

intermediate16 min readUpdated Sep 16, 2026
blog/views.py
python
# blog/views.py
from django.shortcuts import get_object_or_404, render

from .models import Post


def post_detail(request, slug):
    post = get_object_or_404(
        Post.objects.select_related("author"),
        slug=slug,
        status=Post.Status.PUBLISHED,
    )
    return render(request, "blog/post_detail.html", {"post": post})
Released
2005
Language
Python
Pattern
MTV (Model–Template–View)
Database
ORM with migrations
Admin
Built in
Current line
5.x

Why it matters

Why Django ships with everything

Batteries included

The ORM, migrations, templates, forms, authentication, sessions and an admin panel ship in the box, versioned and tested together.

Secure by default

CSRF protection, password hashing, clickjacking headers and SQL-injection-safe queries are on unless you deliberately turn them off.

A real admin for free

Register a model and get a searchable, filterable CRUD interface for staff, which turns internal tools into an afternoon's work.

The big picture

Model, template, view

A model owns the data, a template renders it, and a view connects the two to a request.

Model

Persist

A Python class maps to a table. Fields describe columns, relations describe joins, and migrations evolve the schema.

Template

Render

Django's template language mixes HTML with a deliberately small set of tags and filters, with autoescaping on by default.

View

Handle

A view receives a request and returns a response. It queries the ORM, applies logic and picks a template or a status code.

At a glance

The Django toolbox

Models and ORM

Define classes, get tables. Query with Python instead of string-built SQL.

URL dispatcher

Map paths to views with path() patterns and named routes.

Views

Function-based for clarity, class-based and generic views for reuse.

Admin

A generated back office for every model you register.

Forms

Declarative forms that validate, render and protect against tampering.

Django REST Framework

Serializers, viewsets and routers turn models into JSON APIs.

A short history

From a newsroom tool to a web workhorse

  1. 2003

    Born in a newsroom

    Developers at the Lawrence Journal-World build an internal framework to hit daily deadlines.

    03
  2. 2005

    Public release

    Django is open-sourced and named after the jazz guitarist Django Reinhardt.

    05
  3. 2008

    The Django Software Foundation

    A non-profit takes stewardship, separating the project from any one company.

    08
  4. 2014

    Migrations in core

    Schema migrations become a first-class feature, replacing third-party tools.

    14
  5. 2019

    Async arrives

    Django 3.0 adds ASGI support, opening the door to async views and channels.

    19
  6. Today

    A mature default

    Django 5.x pairs a stable ORM with async support and strong type hints.

    Today

The complete guide

Django: Everything you need to know

What is Django?

Django is a batteries-included web framework for Python. Where most frameworks ask you to choose a database layer, a template engine, an admin panel and an authentication system, Django hands you all of them in one coherent package. You describe your data and your pages, and the framework handles the tedious, security-sensitive parts.

It was built for a newspaper newsroom in 2003 — a team that needed to ship features fast without breaking under deadline. That origin shows. Django favours convention, explicitness and a “there should be one obvious way” attitude, and it has stayed one of the most dependable choices for content-heavy and data-driven sites.

If you have ever spent a week wiring an ORM, a migration tool and an auth system together, Django is the framework that removes that week.

The MTV pattern

Django calls its architecture MTV: Model, Template, View. It is the same idea as MVC with different names, and the difference trips people up at first.

  • A model is a Python class that describes a table and its rows.
  • A template is an HTML file with placeholders that renders the data.
  • A view is a function or class that receives a request, asks the models for data, and returns a response.

The controller in the classic MVC sense is Django itself. The URL dispatcher decides which view runs, and the framework mediates between the three layers. Once that diagram is clear, everything else in Django becomes a detail rather than a mystery.

Models and the ORM

A model is a class that inherits from models.Model. Each attribute is a field, and each field becomes a column.

# blog/models.py
from django.conf import settings
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    body = models.TextField()
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="posts",
    )
    published_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return self.title

CharField needs a max_length because it maps to a bounded VARCHAR. TextField is unbounded. auto_now_add=True sets a value only on creation, while auto_now=True updates on every save. null controls the database, and blank controls form validation; they are separate on purpose.

Relationships are explicit:

  • ForeignKey is many-to-one.
  • ManyToManyField creates a join table.
  • OneToOneField is a unique foreign key.

The related_name sets the reverse accessor, so user.posts.all() works from the other side. The on_delete argument is required because Django refuses to guess what should happen to children when a parent is deleted.

Migrations

Migrations are version-controlled files that describe changes to your schema. You write the model, Django writes the SQL.

python manage.py makemigrations blog
python manage.py migrate

makemigrations inspects your models and produces a migration; migrate applies pending migrations to the database. Because the migration files live in your repository, every environment ends up with the same schema, and changes are reviewable like any other code. Never edit an applied migration — add a new one instead.

Querysets and relationships

The ORM returns querysets, which are lazy: they are not executed until you iterate, slice or evaluate them. That laziness lets you chain filters and only pay for the final query.

Post.objects.filter(status="PB").exclude(author=user).order_by("-published_at")[:10]

Useful queryset methods include filter, exclude, get, first, exists, count, values, values_list, annotate and aggregate. The F() expressions let you reference a column in the database, and Q() objects let you build OR conditions.

from django.db.models import Count, Q

published = Post.objects.filter(
    Q(status="PB") | Q(published_at__isnull=False)
).annotate(comment_count=Count("comments"))

This is where the ORM earns its keep: complex queries stay in Python, are parameterised safely, and compose.

Avoiding the N+1 problem

The single most common performance mistake in Django is the N+1 query. It happens when you access a related object inside a loop, triggering one extra query per row.

# 1 query for posts, then 1 query per post for the author
for post in Post.objects.all():
    print(post.author.username)

select_related joins single-valued relations into the original query, and prefetch_related runs a second query and stitches the results together in Python for multi-valued relations.

# 1 query for posts joined with author, 1 for the comments
posts = Post.objects.select_related("author").prefetch_related("comments")

Reach for select_related on foreign keys and one-to-one fields, and prefetch_related on many-to-many and reverse relations. If a page feels slow, count the queries with the Django Debug Toolbar before optimising anything else.

Views: functions and classes

A view is any callable that takes a request and returns a response. Function-based views are the clearest place to start.

# blog/views.py
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, render

from .models import Post


def post_list(request):
    posts = Post.objects.filter(status="PB").select_related("author")
    return render(request, "blog/post_list.html", {"posts": posts})


def post_api(request, slug):
    post = get_object_or_404(Post, slug=slug)
    return JsonResponse({"title": post.title, "body": post.body})

Class-based views trade a little indirection for reuse. ListView, DetailView, CreateView, UpdateView and DeleteView implement the standard CRUD patterns, and mixins let you compose behaviour.

from django.views.generic import ListView

from .models import Post


class PostListView(ListView):
    model = Post
    template_name = "blog/post_list.html"
    context_object_name = "posts"
    paginate_by = 10

    def get_queryset(self):
        return Post.objects.filter(status="PB").select_related("author")

Use function-based views for one-off logic and generic class-based views for standard list and detail pages. Mixing both in a codebase is normal and healthy.

URLs

The URL dispatcher maps a path to a view. Patterns live in urls.py, and each app can keep its own.

# config/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls")),
]
# blog/urls.py
from django.urls import path

from . import views

app_name = "blog"

urlpatterns = [
    path("", views.post_list, name="post_list"),
    path("<slug:slug>/", views.post_detail, name="post_detail"),
]

Converters such as <int:id> and <slug:slug> validate and type the captured segment. Always name your routes and reverse them with reverse() or the {% url %} tag instead of hardcoding paths, so links survive a rename.

Templates

Templates are HTML with a small, intentionally limited language. Autoescaping is on by default, which is what protects you from XSS.

{% extends "base.html" %}
{% block content %}
  <h1>{{ post.title }}</h1>
  <p>By {{ post.author.username }} on {{ post.published_at|date:"F j, Y" }}</p>
  <div>{{ post.body|linebreaks }}</div>
{% endblock %}

{% extends %} gives template inheritance, {% include %} shares fragments, and filters such as date, linebreaks and default format values. There is no arbitrary Python in templates; if a template needs logic, that logic belongs in the view or the model.

Forms and validation

Django’s form system validates input, renders the HTML and protects against tampering with a CSRF token.

# blog/forms.py
from django import forms

from .models import Post


class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["title", "slug", "body", "status", "published_at"]
        widgets = {"body": forms.Textarea(attrs={"rows": 12})}
# blog/views.py
from django.shortcuts import redirect, render

from .forms import PostForm


def post_create(request):
    if request.method == "POST":
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            return redirect(post)
    else:
        form = PostForm()

    return render(request, "blog/post_form.html", {"form": form})

A ModelForm derives fields and validation from the model, so the two never drift. Clean methods such as clean_slug add custom rules, and form.is_valid() collects every error before you touch the database.

The admin

The admin is Django’s most underrated feature. Register a model and you get a searchable, filterable, permission-aware interface for staff.

# blog/admin.py
from django.contrib import admin

from .models import Post


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "status", "published_at"]
    list_filter = ["status", "created_at"]
    search_fields = ["title", "body"]
    prepopulated_fields = {"slug": ["title"]}
    raw_id_fields = ["author"]
    date_hierarchy = "published_at"

That is a complete internal tool in a dozen lines. For content sites, the admin is often the product. It is also a security boundary: staff permissions, has_add_permission and has_change_permission control exactly what each role can do.

Settings and apps

A Django project is a collection of apps, each a self-contained package with its own models, views, templates and migrations. INSTALLED_APPS lists them, and settings.py holds configuration.

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "blog",
]

Keep settings out of source control secrets. Read them from environment variables, split settings per environment if the project grows, and use django-environ or pydantic-settings to parse them. The django-admin startproject and startapp commands scaffold both layers so the structure is never in question.

Security defaults

Django’s defaults are chosen to make the safe thing the easy thing:

  • CSRF tokens protect state-changing forms; the middleware rejects requests without them.
  • SQL injection is avoided because querysets parameterise values.
  • XSS is mitigated because templates escape variables unless you opt out with |safe.
  • Passwords are hashed with a modern algorithm and configurable hashers.
  • Clickjacking is blocked by X-Frame-Options middleware.
  • HTTPS is enforced in production with SECURE_SSL_REDIRECT, SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE.

Run python manage.py check --deploy before shipping. It flags the settings you forgot, and it is far cheaper than a security review.

Building APIs with Django REST Framework

For JSON APIs, Django REST Framework (DRF) is the standard companion. A serializer describes how a model becomes JSON and back, and a viewset plus a router generates the CRUD endpoints.

# blog/serializers.py
from rest_framework import serializers

from .models import Post


class PostSerializer(serializers.ModelSerializer):
    author = serializers.StringRelatedField()

    class Meta:
        model = Post
        fields = ["id", "title", "slug", "author", "status", "published_at"]
# blog/api.py
from rest_framework import viewsets

from .models import Post
from .serializers import PostSerializer


class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.select_related("author")
    serializer_class = PostSerializer
    lookup_field = "slug"
# blog/urls.py
from rest_framework.routers import DefaultRouter

from .api import PostViewSet

router = DefaultRouter()
router.register("posts", PostViewSet, basename="post")

urlpatterns = router.urls

Serializers validate input the way forms do, viewsets provide list, create, retrieve, update and delete for free, and DRF can generate OpenAPI schemas for your API. Django’s ORM and DRF’s conventions are a well-worn path for production APIs.

Testing

Django’s test runner creates a fresh test database and provides a client that speaks the full request pipeline.

# blog/tests.py
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse

from .models import Post


class PostDetailTests(TestCase):
    def setUp(self):
        user = get_user_model().objects.create_user("ada", password="secret")
        self.post = Post.objects.create(
            title="Hello",
            slug="hello",
            body="First post",
            author=user,
            status=Post.Status.PUBLISHED,
        )

    def test_detail_page_renders(self):
        response = self.client.get(reverse("blog:post_detail", args=["hello"]))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Hello")

    def test_missing_post_returns_404(self):
        response = self.client.get(reverse("blog:post_detail", args=["nope"]))
        self.assertEqual(response.status_code, 404)

TestCase wraps each test in a transaction and rolls it back, so tests stay isolated and fast. Use django.test.Client for views, the ORM directly for models, and pytest-django if you prefer pytest fixtures. Aim to test behaviour and permissions, not implementation details.

Best practices

  • Keep models focused on data and small domain methods; put orchestration in services or views.
  • Always run select_related and prefetch_related where a template touches relations.
  • Use ModelForm and DRF serializers so validation lives in one place.
  • Name every URL and reverse it; never hardcode paths in templates or code.
  • Add database indexes for columns you filter and order by.
  • Keep secrets in environment variables and run check --deploy before release.
  • Write migrations as code review artifacts and never edit an applied one.
  • Test the request pipeline with Client and the domain logic directly.

Common mistakes

  • Iterating over a queryset and touching relations inside the loop, causing N+1 queries.
  • Using null=True on a string field instead of an empty string, then fighting None checks.
  • Putting business logic in templates because the view felt busy.
  • Calling save() in a loop instead of bulk_create or bulk_update.
  • Forgetting on_delete or choosing CASCADE without thinking about data loss.
  • Leaving DEBUG = True and a hardcoded SECRET_KEY in production.
  • Treating the admin as a user-facing frontend instead of a staff tool.
  • Querying with len(queryset) instead of .count(), loading every row into memory.

Where to go next

Django gives you the complete, conventional path to a production Python web app. If you want a lighter core and more choices, read the Flask guide next. If you are building a modern async JSON API and want types to do the validation, move to FastAPI. Whichever you pick, invest in the database underneath: indexing, transactions and query planning matter more than the framework, and the backend roadmap goes deeper. The REST APIs guide covers the design rules that make an API pleasant to consume.

In practice

Four files that make a Django app

A model, a view, a URL map and an admin registration are the core loop of a Django project.

blog/models.py
from django.conf import settings
from django.db import models
from django.urls import reverse


class Post(models.Model):
    class Status(models.TextChoices):
        DRAFT = "DF", "Draft"
        PUBLISHED = "PB", "Published"

    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    body = models.TextField()
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="posts",
    )
    status = models.CharField(
        max_length=2,
        choices=Status.choices,
        default=Status.DRAFT,
    )
    published_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("blog:post_detail", kwargs={"slug": self.slug})

Loading related objects

Accessing a foreign key inside a loop issues one query per row. Fetch the relation up front and the whole page costs a single query.

Prefer
posts = Post.objects.select_related("author")

for post in posts:
    print(post.author.username)
Avoid
posts = Post.objects.all()

for post in posts:
    # one extra query per iteration (the N+1 problem)
    print(post.author.username)

Handling a missing row

get_object_or_404 turns a DoesNotExist exception into a proper 404 response and keeps the view free of try/except noise.

Prefer
from django.shortcuts import get_object_or_404

post = get_object_or_404(Post, slug=slug)
Avoid
from django.http import Http404

try:
    post = Post.objects.get(slug=slug)
except Post.DoesNotExist:
    raise Http404

Trade-offs

Is Django the right default?

Django optimises for completeness and convention. That is a gift on a deadline and a constraint when you want something lean.

Strengths

  • Everything is already there

    Auth, sessions, forms, an ORM, migrations and an admin are maintained by one project, so they fit together without glue code.

  • Security is the default

    The framework defends against the OWASP basics out of the box, and the documented way to do something is usually the safe way.

  • A huge, stable community

    Twenty years of answers, packages and job listings mean almost every problem has a well-trodden solution.

Trade-offs

  • The learning curve is real

    Settings, apps, the ORM and the template language are a lot to absorb at once. The framework rewards patience more than quick experiments.

  • It is monolithic by design

    Django assumes a relational database and a server-rendered or API architecture. Microservices and exotic stacks fight the grain.

  • Async support is young

    The ORM is still largely synchronous, so mixing async views with database work needs care and threads.

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Django?

Our interactive tutorial walks you through Django step by step — with quizzes and real code you can run in the browser.