A comprehensive, interactive reference to Django — the high-level Python web framework that encourages rapid development and clean, pragmatic design.
Django is a high-level Python web framework designed for fast development of secure, maintainable websites. Originally developed at a newspaper company in 2003, it was released publicly in 2005. The framework follows the principle of "batteries included" — providing almost everything needed out of the box.
Django's tagline is "The web framework for perfectionists with deadlines." It abstracts boilerplate tasks so developers focus on business logic, not reinventing the wheel.
Reusable components, auto-generated admin, and powerful ORM slash development time dramatically.
Built-in protection against SQL injection, XSS, CSRF, clickjacking, and more out of the box.
ORM, admin, auth, forms, templates, caching, internationalization — all built in from day one.
Powers Instagram, Pinterest, Mozilla, and Disqus — proven at massive production scale.
Everything is an app. Reuse apps across projects, or install third-party packages from PyPI.
100% Python — no magic compilation, no runtime surprises. Debug with standard Python tooling.
A Django project is the entire web application. A Django app is a self-contained module within it. One project can contain many apps (e.g., blog, accounts, shop).
# Install Django pip install django # Create a new project django-admin startproject mysite # Create a new app inside the project cd mysite python manage.py startapp blog # Run the development server python manage.py runserver
mysite/ │ ├── manage.py # CLI utility for the project ├── mysite/ │ ├── __init__.py │ ├── settings.py # Global configuration │ ├── urls.py # Root URL dispatcher │ ├── asgi.py # ASGI entry point │ └── wsgi.py # WSGI entry point │ └── blog/ # An app ├── __init__.py ├── models.py # Database models ├── views.py # Request handlers ├── urls.py # App URL patterns ├── forms.py # Form classes ├── admin.py # Admin interface config ├── apps.py # App configuration ├── migrations/ # Database migration files └── templates/ # HTML templates
Django uses the Model–View–Template (MVT) pattern — a variation of the classic MVC. Understanding this separation is key to writing clean Django code.
User navigates to a URL. The browser sends a GET or POST request to Django's WSGI/ASGI server.
The request passes through each middleware class in order — authentication, session, security headers, etc.
Django's URL dispatcher matches the path against urlpatterns in urls.py and calls the matching view function.
The view receives the HttpRequest object, queries models, performs logic, and selects a template to render.
The template engine combines the template file with the context data to produce an HTML string.
The response travels back through middleware (in reverse) for any post-processing like compression or cookie setting.
The final HttpResponse (HTML, JSON, redirect, etc.) is sent back to the browser.
A model is a Python class that maps to a database table. Django's ORM (Object-Relational Mapper) lets you interact with the database in pure Python — no raw SQL required, though raw SQL is always available.
from django.db import models from django.contrib.auth.models import User from django.utils import timezone class Post(models.Model): # CharField — for short strings title = models.CharField(max_length=200) # SlugField — URL-friendly identifier slug = models.SlugField(unique=True) # TextField — for long text content body = models.TextField() # DateTimeField — stores date and time published_at = models.DateTimeField(default=timezone.now) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) # BooleanField is_published = models.BooleanField(default=False) # IntegerField view_count = models.PositiveIntegerField(default=0) # ImageField — requires Pillow cover_image = models.ImageField( upload_to='posts/', null=True, blank=True ) # Meta class configures model behavior class Meta: ordering = ['-published_at'] # newest first verbose_name = 'Post' verbose_name_plural = 'Posts' indexes = [ models.Index(fields=['slug']), ] def __str__(self): return self.title def get_absolute_url(self): from django.urls import reverse return reverse('blog:post_detail', kwargs={'slug': self.slug})
After defining or changing a model, always run python manage.py makemigrations and python manage.py migrate to apply changes to the database.
class Category(models.Model): name = models.CharField(max_length=100) slug = models.SlugField(unique=True) class Tag(models.Model): name = models.CharField(max_length=50) class Post(models.Model): title = models.CharField(max_length=200) # ForeignKey — Many-to-One (many posts → one author) # on_delete=CASCADE removes posts when user is deleted author = models.ForeignKey( User, on_delete=models.CASCADE, related_name='posts' ) # ForeignKey — Many-to-One (many posts → one category) # null/blank=True makes it optional category = models.ForeignKey( Category, on_delete=models.SET_NULL, null=True, blank=True, related_name='posts' ) # ManyToManyField — a post can have many tags tags = models.ManyToManyField(Tag, blank=True) class Comment(models.Model): # OneToOneField — each comment has one parent post reply post = models.ForeignKey( Post, on_delete=models.CASCADE, related_name='comments' ) author = models.ForeignKey(User, on_delete=models.CASCADE) body = models.TextField() created_at = models.DateTimeField(auto_now_add=True) # Self-referential FK for nested comments parent = models.ForeignKey( 'self', on_delete=models.CASCADE, null=True, blank=True, related_name='replies' )
from blog.models import Post, Category from django.db.models import Q, F, Count, Avg # ── CREATE ──────────────────────────────────────── post = Post.objects.create( title="Hello World", slug="hello-world", body="My first post." ) # ── READ ───────────────────────────────────────── all_posts = Post.objects.all() published = Post.objects.filter(is_published=True) single = Post.objects.get(slug="hello-world") # Field lookups (double underscore notation) recent = Post.objects.filter(title__icontains="django") older = Post.objects.filter(view_count__gte=100) by_author = Post.objects.filter(author__username="alice") # join! # Q objects for complex OR / AND logic results = Post.objects.filter( Q(title__icontains="python") | Q(title__icontains="django"), is_published=True ) # F expressions — reference field values in queries Post.objects.filter(view_count=F('comment_count')) Post.objects.update(view_count=F('view_count') + 1) # atomic increment # Aggregation stats = Post.objects.aggregate( total=Count('id'), avg_views=Avg('view_count') ) # Annotation — add computed fields per row posts_with_count = Post.objects.annotate( num_comments=Count('comments') ).order_by('-num_comments') # Optimization — select_related (SQL JOIN for FK/O2O) posts = Post.objects.select_related('author', 'category') # Optimization — prefetch_related (for M2M / reverse FK) posts = Post.objects.prefetch_related('tags', 'comments') # Slicing (LIMIT / OFFSET) top5 = Post.objects.order_by('-view_count')[:5] # ── UPDATE ─────────────────────────────────────── Post.objects.filter(slug="hello-world").update(is_published=True) # ── DELETE ─────────────────────────────────────── Post.objects.filter(is_published=False).delete()
class PublishedManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(is_published=True) def with_comments(self): return self.get_queryset().prefetch_related('comments') class Post(models.Model): title = models.CharField(max_length=200) is_published = models.BooleanField(default=False) # Django's default manager objects = models.Manager() # Custom manager — Post.published.all() published = PublishedManager() def save(self, *args, **kwargs): # Auto-generate slug before saving if not self.slug: from django.utils.text import slugify self.slug = slugify(self.title) super().save(*args, **kwargs) # Usage live_posts = Post.published.all() live_with_comments = Post.published.with_comments()
from django.core.validators import MinValueValidator, MaxValueValidator from django.db.models import CheckConstraint, UniqueConstraint, Q class Review(models.Model): rating = models.IntegerField( validators=[MinValueValidator(1), MaxValueValidator(5)] ) class Meta: constraints = [ # DB-level check constraint CheckConstraint( check=Q(rating__gte=1) & Q(rating__lte=5), name='valid_rating_range' ), # Unique together UniqueConstraint( fields=['user', 'product'], name='one_review_per_user' ), ]
Views are the C of Django's MVT — they receive HTTP requests, process data, and return HTTP responses. Django offers both function-based views (FBVs) and class-based views (CBVs).
from django.shortcuts import render, get_object_or_404, redirect from django.http import HttpResponse, Http404 from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_POST from .models import Post from .forms import PostForm def post_list(request): """List all published posts.""" posts = Post.objects.filter( is_published=True ).select_related('author') return render(request, 'blog/post_list.html', { 'posts': posts, }) def post_detail(request, slug): """Show a single post, or 404 if not found.""" post = get_object_or_404(Post, slug=slug, is_published=True) # Atomic view count increment (no race condition) Post.objects.filter(pk=post.pk).update( view_count=models.F('view_count') + 1 ) return render(request, 'blog/post_detail.html', {'post': post}) @login_required def post_create(request): """Handle both display (GET) and submission (POST) of a form.""" 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) # calls get_absolute_url() else: form = PostForm() return render(request, 'blog/post_form.html', {'form': form})
from django.views import View from django.contrib.auth.mixins import LoginRequiredMixin class PostListView(View): def get(self, request): posts = Post.objects.filter(is_published=True) return render(request, 'blog/list.html', {'posts': posts}) class PostCreateView(LoginRequiredMixin, View): # LoginRequiredMixin redirects to login if not authenticated login_url = '/accounts/login/' def get(self, request): return render(request, 'blog/create.html', {'form': PostForm()}) def post(self, request): form = PostForm(request.POST) if form.is_valid(): obj = form.save(commit=False) obj.author = request.user obj.save() return redirect(obj) return render(request, 'blog/create.html', {'form': form})
from django.views.generic import ( ListView, DetailView, CreateView, UpdateView, DeleteView ) from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy class PostListView(ListView): model = Post template_name = 'blog/post_list.html' context_object_name = 'posts' paginate_by = 10 # auto-paginates! def get_queryset(self): return Post.objects.filter(is_published=True) class PostDetailView(DetailView): model = Post slug_field = 'slug' template_name = 'blog/post_detail.html' def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['related'] = Post.objects.filter( category=self.object.category ).exclude(pk=self.object.pk)[:3] return ctx class PostCreateView(LoginRequiredMixin, CreateView): model = Post fields = ['title', 'slug', 'body', 'category', 'tags'] def form_valid(self, form): form.instance.author = self.request.user return super().form_valid(form) class PostDeleteView(LoginRequiredMixin, DeleteView): model = Post success_url = reverse_lazy('blog:post_list')
import json from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods @require_http_methods(["GET"]) def posts_api(request): posts = Post.objects.filter(is_published=True).values( 'id', 'title', 'slug', 'published_at' ) return JsonResponse({'posts': list(posts)}) @csrf_exempt @require_http_methods(["POST"]) def create_post_api(request): try: data = json.loads(request.body) post = Post.objects.create( title=data['title'], body=data['body'] ) return JsonResponse({'id': post.id}, status=201) except (KeyError, json.JSONDecodeError) as e: return JsonResponse({'error': str(e)}, status=400) # Note: For production APIs, consider Django REST Framework (DRF)
Django's Template Language (DTL) is a safe, designer-friendly syntax for generating HTML. Templates support variable interpolation, template tags, filters, and inheritance.
{# This is the base template all pages extend #} <!DOCTYPE html> <html> <head> <title>{% block title %}My Site{% endblock %}</title> </head> <body> <nav>{% include 'partials/nav.html' %}</nav> <main> {% block content %} {% endblock %} </main> </body> </html> {# ────────────────────────────────────────── #} {# templates/blog/post_list.html #} {% extends 'base.html' %} {% block title %}Blog Posts{% endblock %} {% block content %} {# Variables: {{ var }} | Filters: {{ var|filter }} #} <h1>{{ page_title|upper }}</h1> {# for loop #} {% for post in posts %} <article> <h2><a href="{% url 'blog:detail' post.slug %}"> {{ post.title }} </a></h2> <p>{{ post.published_at|date:"F j, Y" }}</p> <p>{{ post.body|truncatewords:30|linebreaks }}</p> {# Loop variables #} {# forloop.counter, forloop.first, forloop.last #} {% if forloop.last %} <hr> {% endif %} </article> {% empty %} <p>No posts found.</p> {% endfor %} {# Pagination #} {% if is_paginated %} {% if page_obj.has_previous %} <a href="?page={{ page_obj.previous_page_number }}">← Prev</a> {% endif %} {% if page_obj.has_next %} <a href="?page={{ page_obj.next_page_number }}">Next →</a> {% endif %} {% endif %} {% endblock %}
| Filter | Example | Output |
|---|---|---|
date | {{ dt|date:"Y-m-d" }} | 2024-03-15 |
truncatewords | {{ text|truncatewords:10 }} | First ten words... |
linebreaks | {{ bio|linebreaks }} | Wraps in <p> tags |
upper/lower | {{ name|upper }} | ALICE |
default | {{ val|default:"N/A" }} | N/A (if falsy) |
length | {{ list|length }} | 5 |
safe | {{ html|safe }} | Disables auto-escaping |
join | {{ tags|join:", " }} | python, django, web |
floatformat | {{ price|floatformat:2 }} | 9.99 |
Django's URL dispatcher maps URL patterns to view functions. URLs are defined using path() and re_path() and organized per-app using include().
from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), # Include app-level URLs with namespace path('blog/', include('blog.urls', namespace='blog')), path('accounts/', include('django.contrib.auth.urls')), path('api/', include('api.urls', namespace='api')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
from django.urls import path, re_path from . import views # app_name activates the 'blog' namespace app_name = 'blog' urlpatterns = [ # path() — exact match. Type converters: str, int, slug, uuid, path path('', views.PostListView.as_view(), name='post_list'), # <int:pk> — captures an integer URL segment path('<int:pk>/', views.PostDetailView.as_view(), name='post_detail_pk'), # <slug:slug> — captures a slug string path('<slug:slug>/', views.PostDetailView.as_view(), name='detail'), # Multiple segments path('<int:year>/<int:month>/<slug:slug>/', views.post_archive_detail, name='archive_detail'), # re_path() for regex patterns re_path(r'^tag/(?P<tag>[\w-]+)/$', views.posts_by_tag, name='by_tag'), path('create/', views.PostCreateView.as_view(), name='create'), path('<int:pk>/edit/', views.PostUpdateView.as_view(), name='edit'), path('<int:pk>/delete/', views.PostDeleteView.as_view(), name='delete'), ] # In templates: {% url 'blog:detail' post.slug %} # In Python: reverse('blog:detail', kwargs={'slug': post.slug})
Django's forms library handles rendering HTML form widgets, validating user input, and cleaning data. ModelForm automatically creates form fields from a model definition.
from django import forms from django.core.exceptions import ValidationError from .models import Post class PostForm(forms.ModelForm): """ModelForm auto-generates fields from Post model.""" class Meta: model = Post fields = ['title', 'slug', 'body', 'category', 'tags'] widgets = { 'title': forms.TextInput(attrs={'class': 'form-input'}), 'body': forms.Textarea(attrs={'rows': 12, 'class': 'form-textarea'}), } help_texts = { 'slug': 'URL-friendly version of the title.' } # Field-level validation: clean_<fieldname> def clean_title(self): title = self.cleaned_data.get('title') if len(title) < 5: raise ValidationError("Title must be at least 5 characters.") return title.strip() # Cross-field validation: clean() def clean(self): cleaned = super().clean() title = cleaned.get('title') slug = cleaned.get('slug') if title and slug: if slug not in title.lower().replace(' ', '-'): self.add_error('slug', 'Slug should derive from the title.') return cleaned class ContactForm(forms.Form): """Plain Form (not model-backed).""" name = forms.CharField(max_length=100) email = forms.EmailField() message = forms.CharField(widget=forms.Textarea) priority = forms.ChoiceField( choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')] )
Django's auto-generated admin interface is one of its most powerful features. Register your models and get a full CRUD interface instantly — customize it with minimal code.
from django.contrib import admin from .models import Post, Comment, Category class CommentInline(admin.TabularInline): """Show comments inline inside the post admin page.""" model = Comment extra = 0 # don't show blank extra forms readonly_fields = ['created_at'] @admin.register(Post) class PostAdmin(admin.ModelAdmin): # Columns shown in the list view list_display = ['title', 'author', 'is_published', 'view_count', 'published_at'] # Clickable link column list_display_links = ['title'] # Toggle fields directly in the list list_editable = ['is_published'] # Sidebar filters list_filter = ['is_published', 'category', 'published_at'] # Search fields (uses LIKE internally) search_fields = ['title', 'body', 'author__username'] # Prepopulate slug from title prepopulated_fields = {'slug': ('title',)} # Readonly fields in edit view readonly_fields = ['view_count', 'created_at', 'updated_at'] # Organize edit form into sections fieldsets = [ (None, {'fields': ['title', 'slug', 'author']}), ('Content', {'fields': ['body', 'cover_image']}), ('Metadata', { 'fields': ['category', 'tags', 'is_published'], 'classes': ['collapse'] # collapsible section }), ('Stats', {'fields': ['view_count', 'created_at', 'updated_at']}), ] # Inline related models inlines = [CommentInline] # Date-based navigation in sidebar date_hierarchy = 'published_at' # Custom admin action actions = ['publish_posts'] @admin.action(description='Publish selected posts') def publish_posts(self, request, queryset): updated = queryset.update(is_published=True) self.message_user(request, f"{updated} post(s) published.")
Run python manage.py createsuperuser and visit /admin/ to see the admin instantly. No frontend work required — great for internal tools and content management.
Django ships with a complete authentication system: user registration, login/logout, passwords, permissions, and groups. It can also be extended with a custom user model.
from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): # Extend the built-in User model bio = models.TextField(blank=True) avatar = models.ImageField( upload_to='avatars/', null=True ) website = models.URLField(blank=True) # settings.py → AUTH_USER_MODEL = 'accounts.User'
from django.contrib.auth.decorators import ( login_required, permission_required, user_passes_test ) @login_required(login_url='/login/') def dashboard(request): return render(request, 'dashboard.html') @permission_required('blog.add_post') def create_post(request): ... @user_passes_test(lambda u: u.is_staff) def staff_only(request): ...
Middleware is a series of hooks into Django's request/response lifecycle. Each middleware component processes every request and response that passes through Django.
import time import logging logger = logging.getLogger(__name__) class RequestTimingMiddleware: """Logs the time taken to process each request.""" def __init__(self, get_response): # Called once on startup self.get_response = get_response def __call__(self, request): # Code here runs BEFORE the view start = time.monotonic() # Call the next middleware / view response = self.get_response(request) # Code here runs AFTER the view duration = time.monotonic() - start logger.info(f"{request.method} {request.path} — {duration:.3f}s") # Add custom response header response['X-Request-Duration'] = f"{duration:.3f}s" return response def process_exception(self, request, exception): # Runs if view raises an exception logger.error(f"Exception on {request.path}: {exception}") return None # None = let Django handle it normally # settings.py MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'myapp.middleware.RequestTimingMiddleware', # ← add yours here ]
Signals allow decoupled components to get notified when certain actions occur. They implement the Observer pattern — senders broadcast a signal; receivers react without the sender needing to know them.
from django.db.models.signals import post_save, pre_delete, m2m_changed from django.dispatch import receiver, Signal from django.contrib.auth import get_user_model from .models import Post, Profile User = get_user_model() # Auto-create a Profile whenever a User is created @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: Profile.objects.create(user=instance) # Send notification email when a post is published @receiver(post_save, sender=Post) def notify_on_publish(sender, instance, created, **kwargs): if not created and instance.is_published: from django.core.mail import send_mail send_mail( subject=f'New post: {instance.title}', message=f'Read it at {instance.get_absolute_url()}', from_email='noreply@example.com', recipient_list=['subscribers@example.com'], ) # Custom signal post_published = Signal() def publish_post(post): post.is_published = True post.save() # Fire custom signal post_published.send(sender=Post, post=post) # Register signals in AppConfig.ready() # blog/apps.py: from django.apps import AppConfig class BlogConfig(AppConfig): name = 'blog' def ready(self): import blog.signals # noqa
Before deploying Django to production, a handful of critical settings changes are required. Never run with DEBUG=True in production.
import os from .base import * # SECURITY DEBUG = False SECRET_KEY = os.environ.get('SECRET_KEY') ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS').split(',') # HTTPS SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SECURE_HSTS_SECONDS = 31536000 SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True # DATABASE (PostgreSQL recommended) DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': os.environ.get('DB_NAME'), 'USER': os.environ.get('DB_USER'), 'PASSWORD': os.environ.get('DB_PASSWORD'), 'HOST': os.environ.get('DB_HOST'), 'PORT': '5432', 'CONN_MAX_AGE': 60, # persistent connections } } # STATIC FILES (whitenoise for serving) STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' # CACHING (Redis) CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.redis.RedisCache', 'LOCATION': os.environ.get('REDIS_URL'), } } # EMAIL EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = os.environ.get('EMAIL_HOST') EMAIL_PORT = 587 EMAIL_USE_TLS = True
| Checklist Item | Dev | Prod |
|---|---|---|
| DEBUG | True | False |
| Database | SQLite | PostgreSQL |
| Secret Key | Hardcoded | Env Var |
| Static Files | Dev Server | Whitenoise/CDN |
| HTTPS | Off | Enforced |
| Caching | None | Redis |
| ALLOWED_HOSTS | ['*'] | Specific domains |
| WSGI/ASGI Server | manage.py | Gunicorn/Uvicorn |
# Dockerfile FROM python:3.12-slim WORKDIR /app # Install dependencies COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy project COPY . . # Collect static files RUN python manage.py collectstatic --noinput # Run with Gunicorn CMD ["gunicorn", "mysite.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4", "--timeout", "120"]
This guide covered the essentials. Explore the official docs at docs.djangoproject.com for the complete reference.