python web framework · since 2005

The Django
Deep Guide

A comprehensive, interactive reference to Django — the high-level Python web framework that encourages rapid development and clean, pragmatic design.

v5.x Latest 12 Chapters 30+ Code Samples
Explore

What is Django?

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.

Rapid Development

Reusable components, auto-generated admin, and powerful ORM slash development time dramatically.

🔒

Secure by Default

Built-in protection against SQL injection, XSS, CSRF, clickjacking, and more out of the box.

📦

Batteries Included

ORM, admin, auth, forms, templates, caching, internationalization — all built in from day one.

🌐

Scalable

Powers Instagram, Pinterest, Mozilla, and Disqus — proven at massive production scale.

🧩

Pluggable Apps

Everything is an app. Reuse apps across projects, or install third-party packages from PyPI.

🐍

Pure Python

100% Python — no magic compilation, no runtime surprises. Debug with standard Python tooling.

Project vs App

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).

🖥️ terminal
# 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

Project Directory Structure

📁 project structure
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

MVT Architecture

Django uses the Model–View–Template (MVT) pattern — a variation of the classic MVC. Understanding this separation is key to writing clean Django code.

🗄️ Model Data & Business Logic
🧠 View Request Processing
🎨 Template HTML Presentation

Request–Response Lifecycle

1

Browser sends HTTP Request

User navigates to a URL. The browser sends a GET or POST request to Django's WSGI/ASGI server.

2

Middleware Processing (in)

The request passes through each middleware class in order — authentication, session, security headers, etc.

3

URL Resolver

Django's URL dispatcher matches the path against urlpatterns in urls.py and calls the matching view function.

4

View Function / Class

The view receives the HttpRequest object, queries models, performs logic, and selects a template to render.

5

Template Rendering

The template engine combines the template file with the context data to produce an HTML string.

6

Middleware Processing (out)

The response travels back through middleware (in reverse) for any post-processing like compression or cookie setting.

7

HTTP Response delivered

The final HttpResponse (HTML, JSON, redirect, etc.) is sent back to the browser.

Models & ORM

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.

Defining a Model

📄 blog/models.py
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.

Model Relationships

📄 blog/models.py — relationships
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'
    )

QuerySet API — The ORM in Action

🐍 orm_examples.py
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()

Custom Managers & QuerySets

📄 blog/models.py — custom managers
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()

Model Validators & Constraints

📄 validators & db constraints
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

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).

📄 blog/views.py — function-based
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})
📄 blog/views.py — class-based
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})

Generic Class-Based Views — The Power Shortcut

📄 blog/views.py — generic CBVs
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')

JSON API Responses

📄 api/views.py — JSON responses
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)

Templates

Django's Template Language (DTL) is a safe, designer-friendly syntax for generating HTML. Templates support variable interpolation, template tags, filters, and inheritance.

📄 templates/base.html — template 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 %}

Built-in Template Filters

FilterExampleOutput
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

URL Routing

Django's URL dispatcher maps URL patterns to view functions. URLs are defined using path() and re_path() and organized per-app using include().

📄 mysite/urls.py — root URLconf
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)
📄 blog/urls.py — app URLconf
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})

Forms

Django's forms library handles rendering HTML form widgets, validating user input, and cleaning data. ModelForm automatically creates form fields from a model definition.

📄 blog/forms.py
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')]
    )

The Django Admin

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.

📄 blog/admin.py
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.

Authentication & Permissions

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.

📄 accounts/models.py
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'
📄 views — auth decorators
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

Middleware is a series of hooks into Django's request/response lifecycle. Each middleware component processes every request and response that passes through Django.

📄 myapp/middleware.py — custom middleware
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

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.

📄 blog/signals.py
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

Deployment Checklist

Before deploying Django to production, a handful of critical settings changes are required. Never run with DEBUG=True in production.

📄 settings/production.py
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 ItemDevProd
DEBUGTrueFalse
DatabaseSQLitePostgreSQL
Secret KeyHardcodedEnv Var
Static FilesDev ServerWhitenoise/CDN
HTTPSOffEnforced
CachingNoneRedis
ALLOWED_HOSTS['*']Specific domains
WSGI/ASGI Servermanage.pyGunicorn/Uvicorn
📄 Dockerfile — containerized deployment
# 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"]

You're now Django-ready.

This guide covered the essentials. Explore the official docs at docs.djangoproject.com for the complete reference.