AlgoMaster Logo

Django Overview

Medium Priority25 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

Django is a batteries-included Python web framework. It ships with an ORM, an admin interface, an auth system, a templating language, a forms library, a migrations tool, and a development server, all bundled into one package and meant to work together. This lesson walks through what Django is, the philosophy behind it, the pieces inside the box, and when Django is the right choice versus something lighter.

What Django Is

Django is a server-side web framework. You write Python classes and functions that describe data models, URL routes, and view logic; Django turns the result into a working web application that talks HTTP, persists to a database, and renders HTML pages. The first version was released in 2005, which makes it one of the older Python frameworks still in active use, and a lot of the design decisions reflect that age: the framework was built for content-heavy sites with admin needs, and most of those defaults still hold today.

The shortest way to describe Django is "a CMS-shaped toolkit for building web apps." The Django authors built it for newspaper sites where editors needed an admin panel, articles needed URLs, pages needed templates, and forms needed validation. Those patterns turned out to fit a wide range of web apps, so Django outgrew its origin and became a general-purpose framework. The historical fingerprint is still visible: the admin site is a major feature, models map cleanly to database tables, and a fresh project comes with everything needed to ship a content site in an afternoon.

The diagram traces a typical request through a Django app. The router picks a view based on the URL, the view talks to the database through the ORM, the result gets passed into a template, and the rendered HTML goes back as the response. Every Django app is a variation on this shape, and most of the framework focuses on making each step ergonomic.

The Django Philosophy

Two slogans show up in the Django docs over and over, and they explain most of the framework's design choices.

The first is "don't repeat yourself," usually shortened to DRY. The same piece of information should live in one place. A model class declares the schema once and Django uses that declaration for the database table, the admin form, the queryset API, the serializer (if you add Django REST Framework), and the form widget. You write the field once, six different parts of the framework use it. The cost of editing a model is one line; the benefit propagates everywhere.

The second is "convention over configuration." Django picks sensible defaults so a working app ships without much configuration. The development server runs on port 8000. The admin URL is /admin/. Migration files live in each app's migrations/ folder. Templates live in templates/. A model called Product maps to a table called appname_product. These conventions let a developer drop into any Django project and find their way around without reading a config file. The trade-off is that Django is opinionated about how things should be structured; fighting the conventions usually costs more than going along with them.

A third theme is "explicit is better than implicit," which Django inherited from the broader Python community. The framework rarely does magic that isn't visible in code. URL routes are declared in a urls.py file, not autodiscovered from view names. Models are explicit Python classes, not generated from the database. Forms are explicit classes with field declarations. Django uses metaprogramming for the ORM's queryset chaining and the admin's auto-generated forms, but the entry point is always an explicit declaration.

Django's opinions are productive for the apps it was built for. If a project's shape doesn't match (a microservice, a pure JSON API, an async-heavy event handler), the friction adds up. The fix is usually to pick a different framework rather than fight Django's grain.

Projects, Apps, and Settings

Django has a two-level structure. A project is the deployable unit: one project equals one running web server. An app is a reusable component inside a project: a chunk of code that owns some models, views, URLs, and templates. A typical Django project contains several apps. A small project might have one app for the catalog, one for customer accounts, and one for orders. A large project has dozens.

The django-admin startproject command creates the project scaffold:

The outer mystore/ is the project directory. The inner mystore/ is the project package, named after the project. settings.py holds configuration (database, installed apps, middleware, secret key). urls.py is the root URL router. wsgi.py and asgi.py are the entry points for sync and async servers respectively. manage.py is the command-line entry point for common tasks: python manage.py runserver, python manage.py migrate, python manage.py createsuperuser.

Adding an app uses python manage.py startapp:

The app has its own models.py, views.py, and admin.py. Adding the app to INSTALLED_APPS in settings.py tells Django to look there for models, templates, and admin registrations.

A minimal model in catalog/models.py:

That class is a complete schema definition. After running python manage.py makemigrations catalog and python manage.py migrate, Django creates the catalog_product table with the right columns and indexes. The __str__ method controls how the object displays in the admin and in shells.

The MTV Pattern

Most web frameworks describe themselves with the MVC pattern (Model, View, Controller). Django uses the same idea but renames the parts to avoid clashing with existing conventions, calling its variant MTV: Model, Template, View.

  • Model: A Python class that defines a database table. class Product(models.Model) with fields like name, price, stock.
  • Template: An HTML file with placeholder syntax for dynamic data. <h1>{{ product.name }}</h1> renders the product's name when the view passes a product into the template context.
  • View: A function or class that takes a request, talks to the model, picks a template, and returns a response. Despite the name, Django's "view" is what most other frameworks would call a controller.

The mismatch with MVC is purely a naming choice. The roles are the same: the model holds data, the view orchestrates the request and response, the template renders the output. The Django docs are explicit that "View" in Django means the thing that handles the request, and the actual presentation logic lives in templates.

A complete view for showing a product detail page:

And the matching template, catalog/templates/catalog/product_detail.html:

render is a helper that loads the template, fills in the context dictionary, and returns an HttpResponse. get_object_or_404 raises a 404 if the product doesn't exist, which Django turns into a "Not Found" response automatically. Three short functions and a short template produce a working page.

The Django ORM (In Brief)

The Django ORM reads and writes database rows by working with model instances instead of SQL. A few snippets show the shape:

The chained calls (Product.objects.filter(...).order_by(...).first()) build a queryset that lazily issues SQL when you iterate or call a terminal method. Field lookups use double-underscore syntax (price__lt, name__icontains, created_at__year), which keeps the query language inside Python expressions instead of SQL strings. What matters here is that the ORM ships with Django and isn't a separate choice.

The Django ORM is convenient but encourages N+1 queries by default. for c in Customer.objects.all(): print(c.orders.count()) issues one query for the customers and one per customer for the order counts. Use prefetch_related("orders") and annotate(order_count=Count("orders")) to collapse it into two queries total.

The Admin Interface

The admin is the feature Django is most known for. It auto-generates a CRUD interface for models, complete with list views, search, filtering, and edit forms, by reading the model definitions. Register a model and the admin appears.

In catalog/admin.py:

That's the minimum. Visit /admin/ in the browser, log in as a superuser, and the generated UI supports list, create, edit, and delete operations on Product rows. No HTML, no forms, no view code required.

A slightly richer admin registration customizes the list view, search, and filters:

That class controls which columns appear in the list, which filters show in the sidebar, which fields the search box matches, and the default sort. Most Django admin pages are a few lines like that. The framework picks up the model's field types and renders the right widgets: text input for CharField, date picker for DateField, dropdown for ForeignKey, multi-select for ManyToManyField.

The admin is a major draw for content sites. An editor team can manage articles, products, or any other data without a custom UI. It isn't a polished customer-facing site (the styling is opinionated and not easy to brand), but as an internal tool it saves weeks of work.

URL Routing

Django routes URLs to view functions through pattern lists. Each app typically has its own urls.py that defines patterns for that app's views, and the project's root urls.py includes those app-level files.

The project's mystore/urls.py:

The app's catalog/urls.py:

path("<int:product_id>/", ...) declares a path converter that captures an integer from the URL and passes it as product_id to the view. Other built-in converters cover strings (<str:slug>), UUIDs (<uuid:id>), and slugs (<slug:slug>). Custom converters can be registered for things like ISBNs or product codes.

Named patterns matter because templates and views can reverse URLs by name instead of hardcoding paths:

In templates:

This decouples the URL structure from the rest of the code. Changing the URL pattern still resolves every reference correctly because they all go through the name lookup.

Templates

Django templates are an HTML-shaped language with double-brace expressions and percent-brace tags. The shape is intentionally restricted compared to Python: templates can read attributes, call methods that don't take arguments, loop, and branch, but cannot run arbitrary Python. The reasoning is that templates should be safe for designers to edit without accidentally running code, and complex logic belongs in the view.

A template that lists products with a conditional:

{% extends %} lets a template inherit from a base layout and override blocks. {% for %} iterates. {% if %} branches. {% empty %} runs when the iterable is empty. The double-brace {{ product.name }} reads the attribute and HTML-escapes the result automatically, which is the default safe-by-default behavior for cross-site scripting protection.

Filters chain on values with the pipe character: {{ product.created_at|date:"Y-m-d" }}, {{ description|truncatewords:30 }}, {{ name|upper }}. Custom filters and tags can be registered in any app and become available across templates.

The Django template language isn't the only option. Jinja2 is also supported through the django.template.backends.jinja2 backend, and many teams prefer Jinja for the more Python-like syntax. The default Django language is fine for most projects and has the smallest learning curve.

Forms Framework

Django's forms library handles the round-trip from HTML form to validated Python data. Declare a form class with field types, render it in a template, accept the submitted data, validate it, and either save the result or show errors.

ModelForm builds the form fields automatically from the model. The view that handles both display and submission:

The template:

form.is_valid() runs every field's validators (type checks, length checks, required checks) and populates form.errors if anything fails. form.save() writes the validated data to the database. The template's {% csrf_token %} inserts a token that Django checks on submission, blocking cross-site request forgery automatically.

Forms also work outside HTML, for example in API code that validates JSON payloads without rendering HTML at all. The pattern stays the same: declare fields, validate, access cleaned data.

Middleware

Middleware is a stack of functions that wrap every request and response. Each middleware can inspect or modify the request before it reaches the view, and inspect or modify the response on the way out. The framework ships with a handful of useful middlewares enabled by default.

That default list handles security headers, session cookies, CSRF protection, authentication, flash messages, and clickjacking defense. Custom middleware is a callable that takes a get_response and returns a callable taking a request:

That middleware times every request and adds an X-Request-Time header to the response. Added to MIDDLEWARE, it runs on every view automatically. Logging, rate limiting, request ID injection, and authentication checks are all common middleware concerns.

Every middleware adds latency to every request. Most are fast (microseconds), but a poorly written one that opens a database connection per request can add tens of milliseconds. Profile the middleware stack before adding more, especially on hot paths.

Auth, Sessions, and Users

Django ships with a working user system. django.contrib.auth provides a User model, login and logout views, password hashing, permissions, and groups. django.contrib.sessions stores per-user state in the database (or cache, or signed cookie) and links it to a session cookie.

The typical login flow uses Django's built-in views:

The User model has fields for username, email, password, first name, last name, and flags for is_staff and is_superuser. Permissions are per-model (catalog.add_product, catalog.change_product, catalog.delete_product, catalog.view_product), generated automatically when you migrate. Groups bundle permissions for assignment.

In a view, request.user returns the current user. A common decorator restricts a view to logged-in users:

The auth system covers most internal sites. For OAuth, social login, or single sign-on, third-party packages (django-allauth, python-social-auth) extend it without replacing it.

Migrations

Schema changes in Django flow through the migrations system. After changing a model, running python manage.py makemigrations compares the current model state to the last recorded state and writes a migration file describing the diff. python manage.py migrate applies those migrations to the database.

A migration is a Python file in the app's migrations/ folder:

Each migration declares its dependencies (which migration must run first) and a list of operations. Operations are forward by default and reverse by inverse, so migrate catalog 0001 rolls back to that version. Migration history lives in a django_migrations table, so the same migration won't run twice.

Auto-generated migrations work for the common cases but sometimes miss things (renames are usually misread as drop-then-add). Review the generated migration before committing.

Running Django

python manage.py runserver starts the development server on http://127.0.0.1:8000/. It auto-reloads on code changes, shows tracebacks in the browser when DEBUG=True, and uses a single-threaded WSGI server underneath. It's fine for development and not fit for production.

Production Django runs behind a WSGI or ASGI server. The two most common choices are gunicorn (WSGI, the traditional Python production server) and uvicorn (ASGI, for async views and websockets).

A typical gunicorn invocation:

That runs four worker processes (each handling requests synchronously), bound to port 8000. Behind that, a reverse proxy like nginx handles static files, TLS termination, and HTTP compression.

For async views (introduced in Django 3.1) and websockets, uvicorn with the ASGI entry point:

Most Django apps still run on WSGI because most Django code is synchronous. The async support is real but uneven: the ORM is async-capable from Django 4.1 onward, but a lot of third-party packages assume sync. For a brand-new project that needs async-first behavior, FastAPI is usually a better starting point. For a project that needs Django's batteries (admin, ORM, auth), sync Django with gunicorn is the typical choice.

runserver is convenient for development but processes one request at a time. Load testing or any "see how the page feels with a real database" check deserves a real WSGI server even locally. The performance numbers from runserver are not representative of production.

When Django Fits (and When It Doesn't)

Django is a strong fit when:

  1. The app is monolithic and content-heavy. Articles, products, comments, users, orders, categories. Django was built for this shape and the admin alone is worth the price of entry.
  2. The team values batteries-included tooling. Auth, sessions, forms, admin, ORM, migrations, caching, internationalization, all in one package. There's no need to pick each piece separately.
  3. The schema is stable enough that a heavyweight migration system pays off. Migrations track every column, every index, every foreign key.
  4. The team is comfortable with Python's sync model. Django can do async, but the docs and most third-party packages still assume sync, and the ORM's async story is newer than the sync one.

Django gets in the way when:

  1. The app is a pure JSON API with no HTML. Django REST Framework helps a lot here, but FastAPI is leaner, async-native, and generates OpenAPI docs without extra setup.
  2. The app is a microservice. Django's footprint (the project structure, the settings file, the installed-app machinery) is heavy for a service that exposes three endpoints.
  3. The workload is async-heavy (websockets, long-poll, streaming). Django's async support is real but partial; FastAPI or Starlette were built for this.
  4. The team wants flexibility over convention. Django's opinions are productive when the team agrees with them, frustrating otherwise.

The pragmatic answer is "Django for monolithic content apps, FastAPI for APIs and microservices, Flask for tiny apps where Django would be overkill." Many teams run all three in different services within the same company.

Quiz

Django Overview Quiz

10 quizzes