Python Django Framework: The Complete Guide for 2026
Django is the most popular Python web framework, powering applications from Instagram and Pinterest to Disqus and Mozilla. It follows the "batteries included" philosophy: everything you need to build a production web application — ORM, authentication, admin panel, form handling, template engine, security middleware — ships with the framework. You do not spend weeks choosing and gluing together libraries. You spend that time building your application.
This guide covers Django from initial project setup through production deployment. Every section includes working code examples that you can adapt for your own projects. Whether you are building a content management system, an e-commerce platform, a SaaS application, or a REST API, Django provides the foundation and Django REST Framework extends it for modern frontend architectures.
Table of Contents
1. Project Setup and Structure
Every Django project starts with a virtual environment and the django-admin startproject command. Keeping dependencies isolated per project prevents version conflicts and makes deployments reproducible.
Installation and Project Creation
# Create and activate a virtual environment
python -m venv venv
source venv/bin/activate # Linux/macOS
# venv\Scripts\activate # Windows
# Install Django
pip install django
# Create the project
django-admin startproject myproject .
# Create your first app
python manage.py startapp blog
# Run the development server
python manage.py runserver
The . after startproject places the project files in the current directory instead of creating a nested folder. This produces a cleaner layout for production projects.
Project Structure
myproject/
manage.py # CLI entry point (runserver, migrate, etc.)
myproject/
__init__.py
settings.py # Configuration (database, apps, middleware)
urls.py # Root URL configuration
wsgi.py # WSGI entry point for production
asgi.py # ASGI entry point for async/WebSockets
blog/
__init__.py
admin.py # Admin panel registration
apps.py # App configuration
models.py # Database models
views.py # Request handlers
urls.py # App-level URL routing (create this)
forms.py # Form definitions (create this)
tests.py # Test cases
templates/ # HTML templates (create this)
blog/
post_list.html
post_detail.html
migrations/ # Database migration files
__init__.py
Essential Settings
# myproject/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Your apps
'blog.apps.BlogConfig',
]
# Database - use PostgreSQL in production
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # dev
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# For production, use PostgreSQL:
# pip install psycopg2-binary
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.postgresql',
# 'NAME': os.environ.get('DB_NAME', 'myproject'),
# 'USER': os.environ.get('DB_USER', 'postgres'),
# 'PASSWORD': os.environ.get('DB_PASSWORD', ''),
# 'HOST': os.environ.get('DB_HOST', 'localhost'),
# 'PORT': os.environ.get('DB_PORT', '5432'),
# }
# }
# Static files
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static']
# Media files (user uploads)
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Security settings for production
# SECRET_KEY = os.environ.get('SECRET_KEY')
# DEBUG = False
# ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
2. Models and the ORM
Django's Object-Relational Mapper (ORM) lets you define database tables as Python classes and query them with Python code instead of raw SQL. Models are the single source of truth for your data structure — Django generates database migrations automatically from model changes.
Defining Models
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils import timezone
from django.utils.text import slugify
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(max_length=100, unique=True)
description = models.TextField(blank=True)
class Meta:
verbose_name_plural = 'categories'
ordering = ['name']
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Post(models.Model):
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique_for_date='published_at')
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='blog_posts'
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='posts'
)
body = models.TextField()
excerpt = models.TextField(blank=True, max_length=500)
status = models.CharField(
max_length=10,
choices=STATUS_CHOICES,
default='draft'
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(default=timezone.now)
class Meta:
ordering = ['-published_at']
indexes = [
models.Index(fields=['-published_at']),
models.Index(fields=['status']),
]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:post_detail', kwargs={'slug': self.slug})
class Comment(models.Model):
post = models.ForeignKey(
Post,
on_delete=models.CASCADE,
related_name='comments'
)
name = models.CharField(max_length=80)
email = models.EmailField()
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=True)
class Meta:
ordering = ['created_at']
def __str__(self):
return f'Comment by {self.name} on {self.post}'
Migrations
# Generate migration files from model changes
python manage.py makemigrations
# Apply migrations to the database
python manage.py migrate
# See the SQL a migration would execute
python manage.py sqlmigrate blog 0001
# Show migration status
python manage.py showmigrations
QuerySet API: Querying Data
# Create objects
post = Post.objects.create(
title='My First Post',
slug='my-first-post',
author=user,
body='Hello, Django!',
status='published'
)
# Retrieve all published posts
published = Post.objects.filter(status='published')
# Chain filters
recent_tech = Post.objects.filter(
status='published',
category__slug='technology',
published_at__gte=timezone.now() - timezone.timedelta(days=30)
)
# Exclude, order, slice
posts = Post.objects.exclude(
status='draft'
).order_by('-published_at')[:10]
# Get a single object (raises DoesNotExist if not found)
post = Post.objects.get(slug='my-first-post')
# Get or 404 (common in views)
from django.shortcuts import get_object_or_404
post = get_object_or_404(Post, slug='my-first-post', status='published')
# Aggregate and annotate
from django.db.models import Count, Avg, Q
# Count posts per category
categories = Category.objects.annotate(
post_count=Count('posts')
).filter(post_count__gt=0)
# Complex queries with Q objects (OR conditions)
results = Post.objects.filter(
Q(title__icontains='django') | Q(body__icontains='django'),
status='published'
)
# Efficient relationship loading
# select_related: follows ForeignKey (single JOIN)
posts = Post.objects.select_related('author', 'category').all()
# prefetch_related: separate query for ManyToMany / reverse FK
posts = Post.objects.prefetch_related('comments').all()
# Update and delete
Post.objects.filter(status='draft', created_at__lt=cutoff).delete()
Post.objects.filter(author=user).update(status='published')
# Raw SQL when needed
posts = Post.objects.raw(
'SELECT * FROM blog_post WHERE status = %s ORDER BY published_at DESC',
['published']
)
3. Views: Function-Based and Class-Based
Views handle HTTP requests and return HTTP responses. Django supports two styles: function-based views (FBVs) for simplicity and explicitness, and class-based views (CBVs) for reusability and reduced boilerplate. Both are fully supported and widely used.
Function-Based Views
# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponse, JsonResponse
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required
from .models import Post, Category
from .forms import CommentForm, PostForm
def post_list(request):
"""List all published posts with pagination."""
posts = Post.objects.filter(
status='published'
).select_related('author', 'category')
# Optional category filter
category_slug = request.GET.get('category')
if category_slug:
posts = posts.filter(category__slug=category_slug)
# Pagination
paginator = Paginator(posts, 10) # 10 posts per page
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
categories = Category.objects.all()
return render(request, 'blog/post_list.html', {
'page_obj': page_obj,
'categories': categories,
})
def post_detail(request, slug):
"""Display a single post with comments and comment form."""
post = get_object_or_404(Post, slug=slug, status='published')
comments = post.comments.filter(active=True)
if request.method == 'POST':
form = CommentForm(request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.post = post
comment.save()
return redirect(post.get_absolute_url())
else:
form = CommentForm()
return render(request, 'blog/post_detail.html', {
'post': post,
'comments': comments,
'form': form,
})
@login_required
def post_create(request):
"""Create a new blog post (authenticated users only)."""
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.get_absolute_url())
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})
Class-Based Views
# blog/views.py
from django.views.generic import (
ListView, DetailView, CreateView, UpdateView, DeleteView
)
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
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):
queryset = Post.objects.filter(
status='published'
).select_related('author', 'category')
category = self.request.GET.get('category')
if category:
queryset = queryset.filter(category__slug=category)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all()
return context
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_queryset(self):
return Post.objects.filter(status='published')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['comments'] = self.object.comments.filter(active=True)
context['form'] = CommentForm()
return context
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class PostUpdateView(LoginRequiredMixin, UpdateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def get_queryset(self):
# Users can only edit their own posts
return Post.objects.filter(author=self.request.user)
class PostDeleteView(LoginRequiredMixin, DeleteView):
model = Post
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('blog:post_list')
def get_queryset(self):
return Post.objects.filter(author=self.request.user)
4. URL Routing
Django's URL dispatcher maps URL patterns to view functions or classes. The convention is to define app-level URL patterns and include them in the root URL configuration.
App-Level URLs
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog' # namespace for URL reversing
urlpatterns = [
# Function-based views
path('', views.post_list, name='post_list'),
path('<slug:slug>/', views.post_detail, name='post_detail'),
path('create/', views.post_create, name='post_create'),
# Or class-based views
# path('', views.PostListView.as_view(), name='post_list'),
# path('<slug:slug>/', views.PostDetailView.as_view(), name='post_detail'),
# path('create/', views.PostCreateView.as_view(), name='post_create'),
# path('<slug:slug>/edit/', views.PostUpdateView.as_view(), name='post_update'),
# path('<slug:slug>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
]
Root URL Configuration
# myproject/urls.py
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),
path('blog/', include('blog.urls')),
path('accounts/', include('django.contrib.auth.urls')),
path('api/', include('blog.api_urls')), # REST API routes
]
# Serve media files in development
if settings.DEBUG:
urlpatterns += static(
settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT
)
URL Patterns and Path Converters
# Django's built-in path converters
path('posts/<int:pk>/', views.post_detail) # Integer
path('posts/<slug:slug>/', views.post_detail) # Slug (letters, numbers, hyphens, underscores)
path('posts/<str:title>/', views.post_detail) # String (any non-empty string, excluding /)
path('posts/<uuid:uuid>/', views.post_detail) # UUID
path('files/<path:file_path>/', views.download) # Path (string including /)
# Reversing URLs in templates
# {% url 'blog:post_detail' slug=post.slug %}
# Reversing URLs in Python code
from django.urls import reverse
url = reverse('blog:post_detail', kwargs={'slug': 'my-post'})
# Returns: '/blog/my-post/'
5. Templates
Django's template engine uses HTML files with special template tags and filters. Templates support inheritance, which eliminates repetition by defining a base layout that child templates extend.
Base Template
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Site{% endblock %}</title>
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
<header>
<nav>
<a href="{% url 'blog:post_list' %}">Blog</a>
{% if user.is_authenticated %}
<a href="{% url 'blog:post_create' %}">New Post</a>
<a href="{% url 'logout' %}">Logout</a>
{% else %}
<a href="{% url 'login' %}">Login</a>
{% endif %}
</nav>
</header>
<main>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% block content %}{% endblock %}
</main>
<footer>
<p>© 2026 My Site</p>
</footer>
{% block extra_js %}{% endblock %}
</body>
</html>
Post List Template
<!-- templates/blog/post_list.html -->
{% extends 'base.html' %}
{% block title %}Blog Posts{% endblock %}
{% block content %}
<h1>Blog</h1>
<!-- Category filter -->
<div class="categories">
<a href="{% url 'blog:post_list' %}">All</a>
{% for cat in categories %}
<a href="?category={{ cat.slug }}">{{ cat.name }}</a>
{% endfor %}
</div>
{% for post in page_obj %}
<article>
<h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
<p class="meta">
By {{ post.author.get_full_name|default:post.author.username }}
on {{ post.published_at|date:"F j, Y" }}
{% if post.category %}in {{ post.category.name }}{% endif %}
</p>
<p>{{ post.excerpt|default:post.body|truncatewords:50 }}</p>
</article>
{% empty %}
<p>No posts yet.</p>
{% endfor %}
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<nav class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
<span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next</a>
{% endif %}
</nav>
{% endif %}
{% endblock %}
Common Template Tags and Filters
<!-- Variables -->
{{ post.title }}
{{ post.body|linebreaks }}
{{ post.created_at|date:"Y-m-d H:i" }}
{{ post.body|truncatewords:30 }}
{{ post.title|lower }}
{{ comment_count|default:"No comments" }}
{{ price|floatformat:2 }}
<!-- Conditionals -->
{% if post.status == 'published' %}
<span class="badge">Published</span>
{% elif post.status == 'draft' %}
<span class="badge">Draft</span>
{% endif %}
<!-- Loops -->
{% for post in posts %}
{{ forloop.counter }}. <!-- 1, 2, 3, ... -->
{{ forloop.first }} <!-- True for first iteration -->
{{ forloop.last }} <!-- True for last iteration -->
{% endfor %}
<!-- Static files -->
{% load static %}
<img src="{% static 'images/logo.png' %}" alt="Logo">
<!-- URL reversing -->
<a href="{% url 'blog:post_detail' slug=post.slug %}">Read more</a>
<!-- CSRF protection (required for all POST forms) -->
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Submit</button>
</form>
<!-- Include another template -->
{% include 'blog/partials/sidebar.html' with categories=categories %}
6. Forms and Validation
Django forms handle rendering HTML form elements, validating submitted data, and converting that data into Python objects. ModelForms generate form fields automatically from your model definitions, eliminating duplication between your models and your forms.
ModelForm
# blog/forms.py
from django import forms
from .models import Post, Comment
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'slug', 'category', 'body', 'excerpt', 'status']
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter post title'
}),
'body': forms.Textarea(attrs={
'class': 'form-control',
'rows': 15,
}),
'excerpt': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Brief summary (optional)'
}),
}
def clean_title(self):
title = self.cleaned_data['title']
if len(title) < 5:
raise forms.ValidationError(
'Title must be at least 5 characters long.'
)
return title
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['name', 'email', 'body']
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'body': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Write your comment...'
}),
}
Custom Form with Validation
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
subject = forms.CharField(max_length=200)
message = forms.CharField(widget=forms.Textarea)
cc_myself = forms.BooleanField(required=False)
def clean_email(self):
email = self.cleaned_data['email']
blocked = ['spam.com', 'throwaway.email']
domain = email.split('@')[1]
if domain in blocked:
raise forms.ValidationError(
'This email domain is not allowed.'
)
return email
def clean(self):
"""Cross-field validation."""
cleaned_data = super().clean()
name = cleaned_data.get('name')
message = cleaned_data.get('message')
if name and message and name.lower() in message.lower():
raise forms.ValidationError(
'Do not include your name in the message body.'
)
return cleaned_data
Handling Forms in Views
from django.contrib import messages
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
# Access cleaned data
name = form.cleaned_data['name']
email = form.cleaned_data['email']
subject = form.cleaned_data['subject']
message = form.cleaned_data['message']
# Send email, save to database, etc.
send_mail(subject, message, email, ['admin@example.com'])
messages.success(request, 'Your message has been sent!')
return redirect('contact')
else:
form = ContactForm()
return render(request, 'contact.html', {'form': form})
7. The Admin Panel
Django's admin is an automatically generated interface for managing your application data. With a few lines of configuration, you get a full CRUD interface with search, filtering, pagination, and bulk actions. It is production-ready for internal tools and content management.
# blog/admin.py
from django.contrib import admin
from .models import Post, Category, Comment
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug']
prepopulated_fields = {'slug': ('name',)}
search_fields = ['name']
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'category', 'status', 'published_at']
list_filter = ['status', 'category', 'published_at', 'author']
search_fields = ['title', 'body']
prepopulated_fields = {'slug': ('title',)}
date_hierarchy = 'published_at'
ordering = ['-published_at']
raw_id_fields = ['author'] # faster than dropdown for many users
# Custom actions
actions = ['make_published', 'make_draft']
@admin.action(description='Mark selected posts as published')
def make_published(self, request, queryset):
count = queryset.update(status='published')
self.message_user(request, f'{count} posts marked as published.')
@admin.action(description='Mark selected posts as draft')
def make_draft(self, request, queryset):
count = queryset.update(status='draft')
self.message_user(request, f'{count} posts marked as draft.')
# Show related comments inline
class CommentInline(admin.TabularInline):
model = Comment
extra = 0
readonly_fields = ['name', 'email', 'created_at']
inlines = [CommentInline]
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ['name', 'email', 'post', 'active', 'created_at']
list_filter = ['active', 'created_at']
search_fields = ['name', 'email', 'body']
actions = ['approve_comments', 'reject_comments']
@admin.action(description='Approve selected comments')
def approve_comments(self, request, queryset):
queryset.update(active=True)
@admin.action(description='Reject selected comments')
def reject_comments(self, request, queryset):
queryset.update(active=False)
# Create a superuser to access the admin
python manage.py createsuperuser
# Then visit http://localhost:8000/admin/
8. Authentication and Users
Django's authentication system handles user accounts, passwords, groups, permissions, and sessions. The most important decision you make at the start of a project is defining a custom User model — changing it later after migrations exist is painful.
Custom User Model
# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
"""Custom user model. Always create this at the start of a project."""
bio = models.TextField(blank=True, max_length=500)
avatar = models.ImageField(upload_to='avatars/', blank=True)
website = models.URLField(blank=True)
def __str__(self):
return self.username
# myproject/settings.py
AUTH_USER_MODEL = 'accounts.User' # Must be set BEFORE first migration
Authentication Views
# myproject/urls.py
from django.contrib.auth import views as auth_views
urlpatterns = [
# Django's built-in auth views provide:
# login, logout, password_change, password_reset
path('accounts/', include('django.contrib.auth.urls')),
]
# This gives you these URL patterns:
# /accounts/login/ [name='login']
# /accounts/logout/ [name='logout']
# /accounts/password_change/ [name='password_change']
# /accounts/password_change/done/ [name='password_change_done']
# /accounts/password_reset/ [name='password_reset']
# /accounts/password_reset/done/ [name='password_reset_done']
# /accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
# /accounts/reset/done/ [name='password_reset_complete']
# settings.py
LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
Registration View
# accounts/views.py
from django.contrib.auth import login
from django.shortcuts import render, redirect
from .forms import UserRegistrationForm
def register(request):
if request.method == 'POST':
form = UserRegistrationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user) # Log in immediately after registration
return redirect('blog:post_list')
else:
form = UserRegistrationForm()
return render(request, 'accounts/register.html', {'form': form})
# accounts/forms.py
from django.contrib.auth.forms import UserCreationForm
from .models import User
class UserRegistrationForm(UserCreationForm):
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
Protecting Views
# Function-based views: use decorators
from django.contrib.auth.decorators import login_required, permission_required
@login_required
def dashboard(request):
return render(request, 'dashboard.html')
@permission_required('blog.add_post', raise_exception=True)
def create_post(request):
# Only users with the blog.add_post permission can access this
pass
# Class-based views: use mixins
from django.contrib.auth.mixins import (
LoginRequiredMixin,
PermissionRequiredMixin,
UserPassesTestMixin
)
class DashboardView(LoginRequiredMixin, TemplateView):
template_name = 'dashboard.html'
class EditorOnlyView(UserPassesTestMixin, ListView):
def test_func(self):
return self.request.user.groups.filter(name='editors').exists()
9. REST APIs with Django REST Framework
Django REST Framework (DRF) is the standard toolkit for building Web APIs in Django. It provides serializers, viewsets, authentication, permissions, pagination, and a browsable API that makes development and debugging significantly faster.
# Install DRF
pip install djangorestframework
# settings.py
INSTALLED_APPS = [
...
'rest_framework',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour',
'user': '1000/hour',
},
}
Serializers
# blog/serializers.py
from rest_framework import serializers
from .models import Post, Category, Comment
class CategorySerializer(serializers.ModelSerializer):
post_count = serializers.IntegerField(read_only=True)
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'description', 'post_count']
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ['id', 'name', 'email', 'body', 'created_at', 'active']
read_only_fields = ['created_at', 'active']
class PostSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField(read_only=True)
category = CategorySerializer(read_only=True)
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(),
source='category',
write_only=True,
required=False
)
comments = CommentSerializer(many=True, read_only=True)
comment_count = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField(
view_name='api:post-detail',
lookup_field='slug'
)
class Meta:
model = Post
fields = [
'id', 'url', 'title', 'slug', 'author',
'category', 'category_id', 'body', 'excerpt',
'status', 'published_at', 'created_at', 'updated_at',
'comments', 'comment_count'
]
read_only_fields = ['created_at', 'updated_at']
def get_comment_count(self, obj):
return obj.comments.filter(active=True).count()
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError(
'Title must be at least 5 characters.'
)
return value
ViewSets and Routers
# blog/api_views.py
from rest_framework import viewsets, permissions, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Count
from .models import Post, Category, Comment
from .serializers import PostSerializer, CategorySerializer, CommentSerializer
class PostViewSet(viewsets.ModelViewSet):
"""
API endpoint for blog posts.
list: GET /api/posts/
create: POST /api/posts/
retrieve: GET /api/posts/{slug}/
update: PUT /api/posts/{slug}/
partial_update: PATCH /api/posts/{slug}/
delete: DELETE /api/posts/{slug}/
"""
serializer_class = PostSerializer
lookup_field = 'slug'
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
search_fields = ['title', 'body']
ordering_fields = ['published_at', 'created_at', 'title']
ordering = ['-published_at']
def get_queryset(self):
queryset = Post.objects.select_related(
'author', 'category'
).prefetch_related('comments')
# Non-staff users only see published posts
if not self.request.user.is_staff:
queryset = queryset.filter(status='published')
# Optional category filter
category = self.request.query_params.get('category')
if category:
queryset = queryset.filter(category__slug=category)
return queryset
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def publish(self, request, slug=None):
"""POST /api/posts/{slug}/publish/"""
post = self.get_object()
post.status = 'published'
post.save()
return Response(PostSerializer(post, context={'request': request}).data)
@action(detail=True, methods=['get', 'post'])
def comments(self, request, slug=None):
"""GET/POST /api/posts/{slug}/comments/"""
post = self.get_object()
if request.method == 'POST':
serializer = CommentSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(post=post)
return Response(serializer.data, status=status.HTTP_201_CREATED)
comments = post.comments.filter(active=True)
serializer = CommentSerializer(comments, many=True)
return Response(serializer.data)
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = CategorySerializer
lookup_field = 'slug'
def get_queryset(self):
return Category.objects.annotate(
post_count=Count('posts', filter=models.Q(posts__status='published'))
)
# blog/api_urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import api_views
app_name = 'api'
router = DefaultRouter()
router.register(r'posts', api_views.PostViewSet, basename='post')
router.register(r'categories', api_views.CategoryViewSet, basename='category')
urlpatterns = [
path('', include(router.urls)),
]
# This generates:
# GET /api/posts/ List posts
# POST /api/posts/ Create post
# GET /api/posts/{slug}/ Retrieve post
# PUT /api/posts/{slug}/ Update post
# PATCH /api/posts/{slug}/ Partial update
# DELETE /api/posts/{slug}/ Delete post
# POST /api/posts/{slug}/publish/ Custom action
# GET /api/posts/{slug}/comments/ List comments
# POST /api/posts/{slug}/comments/ Add comment
# GET /api/categories/ List categories
# GET /api/categories/{slug}/ Retrieve category
10. Middleware
Middleware is a framework of hooks that processes requests and responses globally. Every request passes through every middleware class before reaching your view, and every response passes back through them in reverse order. Django uses middleware for sessions, authentication, CSRF protection, and security headers.
Writing Custom Middleware
# blog/middleware.py
import time
import logging
logger = logging.getLogger(__name__)
class RequestTimingMiddleware:
"""Log the time taken to process each request."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start_time = time.monotonic()
response = self.get_response(request)
duration = time.monotonic() - start_time
logger.info(
'%s %s completed in %.3fs (status %d)',
request.method,
request.path,
duration,
response.status_code
)
# Add timing header for debugging
response['X-Request-Duration'] = f'{duration:.3f}s'
return response
class MaintenanceModeMiddleware:
"""Return a 503 response when maintenance mode is enabled."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
from django.conf import settings
if getattr(settings, 'MAINTENANCE_MODE', False):
if not request.user.is_staff:
from django.http import HttpResponse
return HttpResponse(
'<h1>Under Maintenance</h1><p>We will be back shortly.</p>',
status=503
)
return self.get_response(request)
# 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',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# Custom middleware
'blog.middleware.RequestTimingMiddleware',
]
11. Testing
Django includes a test framework built on Python's unittest module, with additional features like a test client for simulating HTTP requests, test database creation, and transactional isolation between tests. Write tests from the beginning — they catch regressions, document behavior, and give you confidence to refactor.
Model Tests
# blog/tests/test_models.py
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
from blog.models import Post, Category
User = get_user_model()
class PostModelTest(TestCase):
@classmethod
def setUpTestData(cls):
"""Set up data shared by all test methods."""
cls.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
cls.category = Category.objects.create(
name='Technology',
slug='technology'
)
cls.post = Post.objects.create(
title='Test Post Title',
slug='test-post-title',
author=cls.user,
category=cls.category,
body='This is the post body content.',
status='published'
)
def test_post_creation(self):
self.assertEqual(self.post.title, 'Test Post Title')
self.assertEqual(self.post.author.username, 'testuser')
self.assertEqual(self.post.status, 'published')
def test_post_str_method(self):
self.assertEqual(str(self.post), 'Test Post Title')
def test_post_get_absolute_url(self):
url = self.post.get_absolute_url()
self.assertEqual(url, '/blog/test-post-title/')
def test_post_ordering(self):
"""Posts should be ordered by published_at descending."""
older_post = Post.objects.create(
title='Older Post',
slug='older-post',
author=self.user,
body='Older content',
published_at=timezone.now() - timezone.timedelta(days=7)
)
posts = list(Post.objects.all())
self.assertEqual(posts[0], self.post) # Newest first
def test_category_auto_slug(self):
category = Category(name='Web Development')
category.save()
self.assertEqual(category.slug, 'web-development')
View Tests
# blog/tests/test_views.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth import get_user_model
from blog.models import Post
User = get_user_model()
class PostViewTest(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.post = Post.objects.create(
title='Published Post',
slug='published-post',
author=self.user,
body='Published content',
status='published'
)
self.draft_post = Post.objects.create(
title='Draft Post',
slug='draft-post',
author=self.user,
body='Draft content',
status='draft'
)
def test_post_list_view(self):
url = reverse('blog:post_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Published Post')
self.assertNotContains(response, 'Draft Post')
def test_post_detail_view(self):
url = reverse('blog:post_detail', kwargs={'slug': 'published-post'})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Published Post')
def test_draft_post_returns_404(self):
url = reverse('blog:post_detail', kwargs={'slug': 'draft-post'})
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_post_create_requires_login(self):
url = reverse('blog:post_create')
response = self.client.get(url)
self.assertEqual(response.status_code, 302) # Redirect to login
def test_post_create_authenticated(self):
self.client.login(username='testuser', password='testpass123')
url = reverse('blog:post_create')
response = self.client.post(url, {
'title': 'New Post',
'slug': 'new-post',
'body': 'New post content here.',
'status': 'published',
})
self.assertEqual(response.status_code, 302) # Redirect on success
self.assertTrue(Post.objects.filter(slug='new-post').exists())
API Tests
# blog/tests/test_api.py
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.urls import reverse
from django.contrib.auth import get_user_model
from blog.models import Post, Category
User = get_user_model()
class PostAPITest(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser', password='testpass123'
)
self.post = Post.objects.create(
title='API Test Post',
slug='api-test-post',
author=self.user,
body='Content for API testing.',
status='published'
)
def test_list_posts(self):
url = reverse('api:post-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
def test_create_post_unauthenticated(self):
url = reverse('api:post-list')
data = {'title': 'Unauthorized', 'body': 'Content', 'slug': 'unauth'}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_create_post_authenticated(self):
self.client.force_authenticate(user=self.user)
url = reverse('api:post-list')
data = {
'title': 'New API Post',
'slug': 'new-api-post',
'body': 'Created via API.',
'status': 'draft'
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Post.objects.count(), 2)
def test_search_posts(self):
url = reverse('api:post-list')
response = self.client.get(url, {'search': 'API Test'})
self.assertEqual(len(response.data['results']), 1)
# Run tests
python manage.py test
# Run tests for a specific app
python manage.py test blog
# Run a specific test class or method
python manage.py test blog.tests.test_views.PostViewTest.test_post_list_view
# Run with verbosity
python manage.py test --verbosity=2
# Use pytest (popular alternative)
# pip install pytest pytest-django
# pytest --reuse-db -v
12. Deployment
A production Django deployment replaces the development server with a WSGI/ASGI server, serves static files through a reverse proxy, uses a production database, and applies security settings. Here is the standard production stack.
Production Settings
# myproject/settings/production.py
import os
from .base import *
DEBUG = False
SECRET_KEY = os.environ['SECRET_KEY']
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
# Security
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
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
X_FRAME_OPTIONS = 'DENY'
# Database
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', 'localhost'),
'PORT': os.environ.get('DB_PORT', '5432'),
'CONN_MAX_AGE': 600,
}
}
# Static files
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# Cache
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': os.environ.get('REDIS_URL', 'redis://localhost:6379/0'),
}
}
# Logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': '/var/log/django/app.log',
'formatter': 'verbose',
},
},
'root': {
'handlers': ['file'],
'level': 'WARNING',
},
}
Gunicorn (WSGI Server)
# Install
pip install gunicorn
# Run with Gunicorn
gunicorn myproject.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 4 \
--timeout 120 \
--access-logfile /var/log/gunicorn/access.log \
--error-logfile /var/log/gunicorn/error.log
# Workers formula: (2 * CPU_CORES) + 1
# For a 2-core server: 5 workers
Nginx Reverse Proxy
# /etc/nginx/sites-available/myproject
upstream django {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# Static files served directly by Nginx
location /static/ {
alias /var/www/myproject/staticfiles/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Media files
location /media/ {
alias /var/www/myproject/media/;
expires 7d;
}
# Proxy everything else to Gunicorn
location / {
proxy_pass http://django;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
}
client_max_body_size 10M;
}
Docker Deployment
# Dockerfile
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Install system dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq-dev && \
rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY . .
# Collect static files
RUN python manage.py collectstatic --noinput
# Create non-root user
RUN addgroup --system app && adduser --system --group app
USER app
EXPOSE 8000
CMD ["gunicorn", "myproject.wsgi:application", \
"--bind", "0.0.0.0:8000", \
"--workers", "4", \
"--timeout", "120"]
# docker-compose.yml
services:
web:
build: .
env_file: .env
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
ports:
- "8000:8000"
restart: always
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
restart: always
redis:
image: redis:7-alpine
restart: always
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./staticfiles:/var/www/static:ro
- ./media:/var/www/media:ro
depends_on:
- web
restart: always
volumes:
pgdata:
Deployment Checklist
# Django's built-in deployment check
python manage.py check --deploy
# This checks for common security issues:
# - DEBUG = True
# - Missing SECRET_KEY
# - Insecure ALLOWED_HOSTS
# - Missing security middleware
# - Missing HTTPS settings
# - Missing HSTS settings
13. Best Practices
Project Structure
- Always create a custom User model — even if you do not need extra fields yet. Use
AbstractUserand setAUTH_USER_MODELbefore your first migration. Changing the User model later is extremely difficult. - Split settings by environment — use a
settings/package withbase.py,development.py, andproduction.py. Never commit secrets to version control. - Keep apps small and focused — each app should handle one domain concept (blog, accounts, payments). If an app has more than 10 models, consider splitting it.
- Use environment variables for configuration —
SECRET_KEY, database credentials, API keys, andDEBUGshould all come from environment variables or a.envfile (never committed).
Database and Models
- Add database indexes — use
db_index=Trueon fields you filter by frequently, andclass Meta: indexes = [...]for composite indexes. - Use
select_related()andprefetch_related()— the N+1 query problem is the most common Django performance issue. Useselect_relatedfor ForeignKey fields andprefetch_relatedfor ManyToMany and reverse relations. - Use
F()andQ()objects —F()expressions perform operations at the database level (avoiding race conditions), andQ()objects enable complex OR/AND queries. - Never use
null=Trueon string fields — useblank=Trueinstead. Django convention is to store empty strings, not NULL, for text fields.
Security
- Never expose
DEBUG=Truein production — it leaks source code, settings, and database queries in error pages. - Always use
{% csrf_token %}in forms — Django's CSRF protection is enabled by default. Do not disable it. - Validate and sanitize all input — use Django forms for validation. Never construct raw SQL queries with string formatting.
- Use Django's built-in password hashing — PBKDF2 with SHA256 by default. Add Argon2 for stronger hashing (
pip install argon2-cffi). - Run
python manage.py check --deploy— before every production deployment to catch security misconfigurations.
Performance
- Use caching — cache expensive queries with Django's cache framework. Use
@cache_pagefor entire views, orcache.get()/set()for specific data. - Use pagination — never return unbounded querysets. Paginate list views and API endpoints.
- Use async views for I/O-bound operations — Django 4.1+ supports async views natively. Use them for views that make external API calls or handle WebSockets.
- Use
django-debug-toolbarin development — it shows SQL queries, template rendering time, cache hits, and signal handlers for every request.
# Install django-debug-toolbar for development
pip install django-debug-toolbar
# settings/development.py
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
INTERNAL_IPS = ['127.0.0.1']
# Example: caching a view
from django.views.decorators.cache import cache_page
@cache_page(60 * 15) # Cache for 15 minutes
def post_list(request):
posts = Post.objects.filter(status='published')
return render(request, 'blog/post_list.html', {'posts': posts})
# Example: manual caching
from django.core.cache import cache
def get_popular_posts():
key = 'popular_posts'
posts = cache.get(key)
if posts is None:
posts = list(
Post.objects.filter(status='published')
.annotate(comment_count=Count('comments'))
.order_by('-comment_count')[:10]
)
cache.set(key, posts, 60 * 30) # Cache for 30 minutes
return posts
Frequently Asked Questions
What is the difference between Django and Flask?
Django is a full-featured, batteries-included web framework that provides an ORM, admin panel, authentication system, form handling, and template engine out of the box. Flask is a micro-framework that gives you a routing system and leaves everything else as a choice you make yourself. Use Django when you are building a data-driven application with user accounts, an admin interface, and complex database models — Django's conventions and built-in components save significant development time. Use Flask when you need a lightweight API, a small microservice, or when you want full control over every component in the stack. For most web applications that involve a database and user authentication, Django is the faster path to production because you spend time building features instead of assembling infrastructure from separate libraries.
How do I deploy a Django application to production?
A production Django deployment requires several components working together: a WSGI server (Gunicorn is the standard) or ASGI server (Uvicorn for async) to run the Django application, a reverse proxy (Nginx or Caddy) to handle static files, SSL termination, and load balancing, and a production database (PostgreSQL is the standard choice for Django). Key steps include collecting static files with python manage.py collectstatic, running python manage.py migrate, setting DEBUG=False, configuring ALLOWED_HOSTS, using a strong SECRET_KEY from environment variables, and enabling HTTPS with all associated security headers. Run python manage.py check --deploy to catch common security misconfigurations. For containerized deployments, use Docker with a multi-stage build and Docker Compose to orchestrate the web server, database, and cache services.
What is Django REST Framework and when should I use it?
Django REST Framework (DRF) is a powerful, flexible toolkit for building Web APIs on top of Django. It provides serializers for converting complex data types (like Django model instances) to and from JSON, viewsets that dramatically reduce boilerplate for standard CRUD operations, a browsable API interface for development and debugging, authentication classes (token, session, and JWT via third-party packages), permission classes for fine-grained access control, plus pagination, filtering, throttling, and content negotiation. Use DRF whenever your Django application needs to serve data to a frontend framework like React, Vue, or Angular, or to mobile applications. DRF integrates seamlessly with Django's ORM and authentication system, so you get the full power of Django's model layer and security while exposing clean, well-documented REST endpoints.
How does Django's ORM compare to writing raw SQL?
Django's ORM translates Python code into optimized SQL queries, providing database abstraction, automatic SQL injection prevention, and database portability (switch from SQLite to PostgreSQL without changing code). For most CRUD operations, the ORM is both safer and more productive than raw SQL: a query like Article.objects.filter(status='published', created__gte=last_week).order_by('-created') is cleaner and more maintainable than the equivalent SQL string. The ORM also handles schema migrations, foreign key relationships, and complex joins through select_related() and prefetch_related(). However, for complex reporting queries, multi-table aggregations, or performance-critical bulk operations, raw SQL may be more efficient and readable. Django supports raw queries via Model.objects.raw() and direct database cursors via django.db.connection for cases where the ORM generates suboptimal SQL.
How do I handle authentication and user management in Django?
Django includes a complete authentication system out of the box: a User model with password hashing (PBKDF2 by default, upgradable to Argon2), login and logout views, password reset with email verification, session management, and a permission and group system for authorization. For most projects, start by creating a custom User model that inherits from AbstractUser (do this before your first migration), then use Django's built-in auth views (LoginView, LogoutView, PasswordResetView) which handle the complex logic of secure authentication. Protect views with the @login_required decorator or LoginRequiredMixin for class-based views. For API authentication with Django REST Framework, use TokenAuthentication or integrate JWT via djangorestframework-simplejwt. For social login (Google, GitHub, etc.), the django-allauth package is the standard solution.
Conclusion
Django is the framework you reach for when you want to build something real and ship it to production. Its batteries-included philosophy means you start with a working authentication system, a database ORM that handles migrations, an admin panel for content management, and security middleware that protects against the most common web vulnerabilities — all from day one.
If you are starting a new project, follow this path: create a virtual environment, install Django, create a custom User model before your first migration, define your models, register them in the admin, and build your views and templates. Use Django REST Framework when you need an API for a frontend framework or mobile application. Write tests from the beginning. Deploy with Gunicorn behind Nginx, or use Docker for containerized deployments.
If you are already working with Django, invest in performance optimization: use select_related() and prefetch_related() to eliminate N+1 queries, add database indexes on fields you filter frequently, implement caching for expensive operations, and install django-debug-toolbar to see exactly what your application is doing on every request.
Django's ecosystem is mature, its documentation is excellent, and its community is one of the most welcoming in open source. The skills you build here transfer directly to production applications used by millions of people.
Related Resources
- Python: The Complete Beginner's Guide — learn Python fundamentals before diving into Django
- REST API Design: The Complete Guide — best practices for designing the API endpoints your Django REST Framework serves
- Docker: The Complete Guide — containerize your Django application for consistent deployments
- JSON vs YAML vs TOML — understand the configuration formats used in Django settings and Docker Compose
- Python Basics Cheat Sheet — quick reference for Python syntax and built-in functions
- Python String Methods Cheat Sheet — essential string operations for template and data processing