🐍 Latest Edition
📖 Beginner to Advanced
⏱️ 50 min read
🎯 20+ Sections
⏱️ Estimated reading time: 45-50 minutes
📋 Quick Summary: Django is the most popular Python web framework powering Instagram, Pinterest, Mozilla, and Spotify. By the end of this course, you will go from zero to deploying production-ready Django applications with authentication, databases, REST APIs, and deployment. No fluff — just what you actually need to build real web apps.
🐍 What Is Django?
Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. Built by experienced developers at the Lawrence Journal-World newspaper in 2003-2005, it was open-sourced in 2005 and has since grown into the most popular Python web framework on the planet.
Django follows the “batteries-included” philosophy, meaning it comes with almost everything you need built-in: ORM, authentication, admin panel, forms, security, templating, and more. No need to stitch together 20 different libraries to get started.
Who Uses Django?
- Instagram — Handles 500M+ daily active users on Django
- Pinterest — 450M+ monthly active users
- Mozilla — Firefox Sync, add-ons marketplace
- Spotify — Internal tools and data pipelines
- The Washington Post — Content management systems
- YouTube — (Parts of it, back when Python was the primary language)
💡 Did You Know? Django was named after the jazz guitarist Django Reinhardt. The framework’s creators were fans, and they wanted a name that was short, unique, and easy to say. It’s pronounced JANG-goh (the D is silent).
🤔 Common Myths About Django Debunked
| Myth | Reality |
|---|---|
| “Django is too heavy for small projects” | Django scales down just as well as it scales up. A basic Django app is ~5 files. |
| “Django is dying / outdated” | Django 5.x just released. GitHub stars: 80K+. Job demand is growing 10% YoY. |
| “You need a lot of Python experience first” | Basic Python syntax is enough. Django’s design lets you learn as you build. |
| “Django doesn’t support async” | Django 3.0+ has full async support. Views, ORM, middleware — all async-ready. |
| “ORMs are evil, raw SQL is better” | Django ORM generates optimized SQL. For complex queries, raw() exists. |
🔧 Environment Setup
Before writing any code, let’s get your environment set up properly. This is the same setup professional Django developers use.
Step 1: Python Installation
Django 5.x requires Python 3.10+. Check your version:
python3 --version # Should be >= 3.10
Step 2: Virtual Environment
Always use a virtual environment. This isolates dependencies per project:
# Create virtual environment python3 -m venv venv # Activate it (Linux/Mac) source venv/bin/activate # Activate it (Windows) venv\Scripts\activate # Your prompt should now show (venv) at the start
Step 3: Install Django
pip install django # Verify installation django-admin --version # Should output 5.x
Step 4: Create Your First Project
django-admin startproject myproject cd myproject python manage.py runserver
Open http://127.0.0.1:8000 in your browser. You should see the Django welcome page with the rocket. 🚀
💡 Did You Know? The
django-admincommand is a wrapper around Django’s management utility. You can see all available commands by runningdjango-admin help. Django ships with 40+ built-in management commands.
📁 Django Project Structure
Understanding Django’s project structure is critical. Let’s break down what startproject creates:
myproject/ ├── manage.py # Command-line utility for interacting with project ├── myproject/ # Project package (settings, URLs, WSGI) │ ├── __init__.py # Makes directory a Python package │ ├── settings.py # ALL project configuration lives here │ ├── urls.py # URL declarations (like a table of contents) │ ├── asgi.py # ASGI config for async servers │ └── wsgi.py # WSGI config for traditional servers └── db.sqlite3 # Default SQLite database (auto-created)
What Each File Does
- manage.py — Same as
django-admin, auto-configuresDJANGO_SETTINGS_MODULEfor your project. You’ll run almost everything through this. - settings.py — The brain of your Django project. Database config, installed apps, middleware, templates, static files, etc.
- urls.py — Maps URLs to views. Think of it as the router.
- wsgi.py / asgi.py — Entry points for web servers to serve your app.
Creating Your First App
Django projects are organized into apps. An app handles one specific functionality:
python manage.py startapp blog
This creates:
blog/ ├── __init__.py ├── admin.py # Register models for admin panel ├── apps.py # App configuration ├── migrations/ # Database migration files │ └── __init__.py ├── models.py # Define your data models (database tables) ├── tests.py # Write tests └── views.py # HTTP request handlers (views)
After creating an app, register it in settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog', # Your app goes here
]
💡 Did You Know? Django apps are designed to be reusable. You can unplug an entire app from one project and plug it into another. That’s why the convention is to make each app self-contained and focused on one thing only.
🌐 Views & URL Routing
Views are where the magic happens. A view is a Python function (or class) that takes a web request and returns a web response.
Function-Based Views (FBVs)
The simplest form of a view:
# blog/views.py
from django.http import HttpResponse
from django.shortcuts import render
def home(request):
return HttpResponse("Hello, Django!")
def about(request):
return render(request, 'about.html', {'title': 'About Us'})
def post_detail(request, slug):
# Dynamic URL parameter example
return HttpResponse(f"Post slug: {slug}")
URL Configuration
Create blog/urls.py:
# blog/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='home'),
path('about/', views.about, name='about'),
path('post/<slug:slug>/', views.post_detail, name='post-detail'),
]
Then include app URLs in the project:
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('blog.urls')),
]
URL Path Converters
Django provides built-in path converters for URL parameters:
| Converter | Matches | Example |
|---|---|---|
str |
Any non-empty string, excl. / |
posts/<str:name>/ |
int |
Zero or positive integer | posts/<int:id>/ |
slug |
ASCII letters, numbers, hyphens, underscores | posts/<slug:slug>/ |
uuid |
UUID format | items/<uuid:id>/ |
path |
Any string, including / |
path/<path:path>/ |
Class-Based Views (CBVs)
Django also supports class-based views which promote code reuse through inheritance:
# blog/views.py
from django.views.generic import ListView, DetailView
from .models import Post
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
💡 Did You Know? Django ships with 30+ generic class-based views.
ListView,DetailView,CreateView,UpdateView,DeleteView,FormView— they handle 80% of your use cases out of the box.
🎨 Templates & Static Files
Django’s template engine lets you generate HTML dynamically using Python-like syntax, but with security in mind (auto-escaping, no arbitrary code execution).
Template Configuration
Create a templates/ directory in your app and configure settings.py:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # Project-level templates
'APP_DIRS': True, # App-level templates
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
Template Syntax
<!-- blog/templates/blog/post_list.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<h1>{{ title }}</h1>
{% for post in posts %}
<article>
<h2>
<a href="{% url 'post-detail' post.slug %}">
{{ post.title }}
</a>
</h2>
<p>{{ post.content|truncatewords:50 }}</p>
<small>Published: {{ post.created_at|date:"F j, Y" }}</small>
</article>
{% empty %}
<p>No posts yet.</p>
{% endfor %}
{% if is_paginated %}
<div 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 %}
</div>
{% endif %}
</body>
</html>
Template Inheritance
The most powerful feature — define a base template and extend it:
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My Blog</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<nav>
<a href="{% url 'home' %}">Home</a>
<a href="{% url 'about' %}">About</a>
</nav>
<main>
{% block content %}
{% endblock %}
</main>
<script src="{% static 'js/main.js' %}"></script>
</body>
</html>
<!-- blog/templates/blog/post_list.html -->
{% extends 'base.html' %}
{% block title %}Latest Posts — My Blog{% endblock %}
{% block content %}
<h1>Latest Posts</h1>
{% for post in posts %}
<!-- post markup -->
{% endfor %}
{% endblock %}
Static Files
Configure static files in settings.py:
STATIC_URL = '/static/' STATICFILES_DIRS = [BASE_DIR / 'static'] STATIC_ROOT = BASE_DIR / 'staticfiles' # For production
Organize static files as static/css/style.css, static/js/main.js, etc.
Built-in Template Filters
| Filter | What It Does | Example |
|---|---|---|
date |
Formats a date | {{ post.date|date:"Y-m-d" }} |
default |
Default if value is falsy | {{ bio|default:"No bio" }} |
length |
Length of iterable | {{ posts|length }} |
slugify |
Converts to URL slug | {{ title|slugify }} |
safe |
Marks HTML as safe (no escaping) | {{ content|safe }} |
truncatewords |
Truncates to N words | {{ text|truncatewords:30 }} |
🗄️ Models, ORM & Databases
Django’s ORM (Object-Relational Mapper) is one of the best in the industry. It lets you define your data models as Python classes, and Django handles creating tables, querying, and relationships for you.
Defining Models
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils.text import slugify
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
class Meta:
verbose_name_plural = 'Categories'
def __str__(self):
return self.name
class Post(models.Model):
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='posts')
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
views = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['slug']),
models.Index(fields=['status', 'created_at']),
]
def __str__(self):
return self.title
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
class Comment(models.Model):
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)
active = models.BooleanField(default=True)
class Meta:
ordering = ['created_at']
def __str__(self):
return f'Comment by {self.author} on {self.post}'
Field Types Reference
| Field Type | Database Type | Use Case |
|---|---|---|
CharField |
VARCHAR | Short strings (titles, names, emails) |
TextField |
TEXT | Long-form content (blog posts, comments) |
IntegerField |
INTEGER | Counts, ages, numeric IDs |
BooleanField |
BOOL | Flags, active/inactive toggles |
DateTimeField |
DATETIME | Timestamps (auto_now, auto_now_add) |
ForeignKey |
INTEGER + INDEX | Many-to-one relationships |
ManyToManyField |
Junction table | Many-to-many (tags, likes) |
OneToOneField |
INTEGER + UNIQUE | One-to-one (profiles, settings) |
Migrations
After defining models, create and apply migrations:
# Create migration files (detects model changes) python manage.py makemigrations # See what SQL will be executed python manage.py sqlmigrate blog 0001 # Apply migrations to the database python manage.py migrate # List all migrations python manage.py showmigrations
Querying with the ORM
Django ORM provides a rich query API. Here are the most common patterns:
# CRUD Operations
from blog.models import Post, Category
from django.utils import timezone
# CREATE
post = Post.objects.create(
title='Hello Django',
content='First post!',
author=user,
category=category
)
# READ (with filtering)
published = Post.objects.filter(status='published')
recent = Post.objects.filter(created_at__gte=timezone.now() - timedelta(days=7))
by_category = Post.objects.filter(category__slug='python')
search = Post.objects.filter(title__icontains='django')
single = Post.objects.get(slug='hello-django')
# UPDATE
post.title = 'Updated Title'
post.save()
# Or bulk update
Post.objects.filter(status='draft').update(status='published')
# DELETE
post.delete()
# AGGRETATIONS & ANNOTATIONS
from django.db.models import Count, Sum, Avg, F, Q
# Count posts per category
categories = Category.objects.annotate(post_count=Count('posts'))
# Top authors by post count
top_authors = User.objects.annotate(
post_count=Count('posts')
).filter(post_count__gt=5).order_by('-post_count')
# Complex Q queries
results = Post.objects.filter(
Q(title__icontains='django') | Q(content__icontains='django'),
status='published'
)
# Increment field atomically (no race conditions!)
Post.objects.filter(id=post.id).update(views=F('views') + 1)
# Select related (prevents N+1 queries)
posts = Post.objects.select_related('author', 'category').all()
for post in posts:
print(post.author.username) # No extra query!
💡 Did You Know? The Django ORM executes queries lazily. When you write
Post.objects.filter(...), no SQL is run until you actually iterate over the results or call.get(),.count(),.exists(), etc. This lets you chain filters without hitting the database.
⚙️ Django Admin Panel
Django’s admin panel is one of the biggest productivity boosts. With zero configuration, you get a full CRUD interface for your models.
Basic Registration
# blog/admin.py from django.contrib import admin from .models import Post, Category, Comment admin.site.register(Post) admin.site.register(Category) admin.site.register(Comment)
Advanced Admin Customization
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
# List view customization
list_display = ('title', 'author', 'status', 'category', 'views', 'created_at')
list_filter = ('status', 'category', 'created_at')
search_fields = ('title', 'content')
ordering = ('-created_at',)
date_hierarchy = 'created_at'
# Inline editing
list_editable = ('status',)
list_per_page = 25
# Pre-populate slug from title
prepopulated_fields = {'slug': ('title',)}
# Custom actions
actions = ['make_published']
@admin.action(description='Mark selected posts as published')
def make_published(self, request, queryset):
updated = queryset.update(status='published')
self.message_user(request, f'{updated} posts published.')
# Detail view fieldsets
fieldsets = (
('Content', {
'fields': ('title', 'slug', 'content')
}),
('Metadata', {
'fields': ('author', 'category', 'status')
}),
('Statistics', {
'fields': ('views',),
'classes': ('collapse',) # Collapsible
}),
)
# Permission control
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(author=request.user)
Creating a Superuser
python manage.py createsuperuser # Follow prompts: username, email, password # Then visit http://127.0.0.1:8000/admin/
📝 Forms & Validation
Django Forms handle HTML form rendering, validation, and processing — all in one place.
ModelForm (Most Common)
# blog/forms.py
from django import forms
from .models import Post, Comment
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'category', 'status']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Enter title'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
'category': forms.Select(attrs={'class': 'form-control'}),
'status': forms.Select(attrs={'class': 'form-control'}),
}
def clean_title(self):
title = self.cleaned_data['title']
if len(title) < 10:
raise forms.ValidationError('Title must be at least 10 characters.')
return title
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['body']
widgets = {
'body': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Leave a comment...'
}),
}
def clean_body(self):
body = self.cleaned_data['body']
if len(body.strip()) < 2:
raise forms.ValidationError('Comment is too short.')
return body
Using Forms in Views
# Using FormView generic CBV
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy
class PostCreateView(CreateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
success_url = reverse_lazy('post-list')
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
# Function-based approach
def create_post(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect('post-detail', slug=post.slug)
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})
🔐 Authentication & Authorization
Django ships with a full authentication system: users, groups, permissions, sessions, and password management.
Built-in Auth Views
# urls.py
from django.contrib.auth import views as auth_views
urlpatterns += [
path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('password-change/', auth_views.PasswordChangeView.as_view(
template_name='registration/password_change.html'
), name='password-change'),
path('password-reset/', auth_views.PasswordResetView.as_view(
template_name='registration/password_reset.html'
), name='password-reset'),
path('password-reset/done/', auth_views.PasswordResetDoneView.as_view(
template_name='registration/password_reset_done.html'
), name='password-reset-done'),
]
Registration Form
# forms.py
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
class RegisterForm(UserCreationForm):
email = forms.EmailField(required=True)
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
def save(self, commit=True):
user = super().save(commit=False)
user.email = self.cleaned_data['email']
if commit:
user.save()
return user
Protecting Views
# Decorator-based (FBV)
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import permission_required
@login_required
@permission_required('blog.add_post', raise_exception=True)
def create_post(request):
# Only logged-in users with permission can access
pass
# Mixin-based (CBV)
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
class PostCreateView(LoginRequiredMixin, CreateView):
login_url = '/login/'
redirect_field_name = 'next'
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
def test_func(self):
post = self.get_object()
return self.request.user == post.author # Only author can edit
🔗 Middleware & Signals
Middleware
Middleware is a chain of hooks that process requests and responses globally. Django's request/response cycle passes through each middleware layer:
# Custom middleware — track request time
import time
class RequestTimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Code executed BEFORE view
start_time = time.time()
response = self.get_response(request)
# Code executed AFTER view
duration = time.time() - start_time
print(f"Request to {request.path} took {duration:.3f}s")
response['X-Response-Time'] = str(duration)
return response
Signals
Signals allow decoupled apps to get notified when certain actions occur:
# blog/signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.contrib.auth.models import User
from django.core.mail import send_mail
from .models import Post
@receiver(post_save, sender=Post)
def notify_new_post(sender, instance, created, **kwargs):
if created:
# Send email notification for new posts
send_mail(
subject=f'New Post: {instance.title}',
message=f'A new post was published by {instance.author}',
from_email='noreply@blog.com',
recipient_list=['admin@blog.com'],
)
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
Register signals in apps.py:
# blog/apps.py
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'blog'
def ready(self):
import blog.signals # noqa
🔌 REST APIs with Django REST Framework
Django REST Framework (DRF) is the most popular toolkit for building REST APIs in Django. It powers APIs for companies like Mozilla, Heroku, and Eventbrite.
Installation
pip install djangorestframework
# settings.py
INSTALLED_APPS = [
...
'rest_framework',
]
Serializers
Serializers convert complex data types (like querysets) to JSON and back:
# blog/serializers.py
from rest_framework import serializers
from .models import Post, Comment
class CommentSerializer(serializers.ModelSerializer):
author_username = serializers.ReadOnlyField(source='author.username')
class Meta:
model = Comment
fields = ['id', 'author', 'author_username', 'body', 'created_at']
read_only_fields = ['author']
class PostSerializer(serializers.ModelSerializer):
author_username = serializers.ReadOnlyField(source='author.username')
comments = CommentSerializer(many=True, read_only=True)
comment_count = serializers.SerializerMethodField()
class Meta:
model = Post
fields = [
'id', 'title', 'slug', 'author', 'author_username',
'category', 'content', 'status', 'views',
'comment_count', 'comments', 'created_at', 'updated_at'
]
read_only_fields = ['author', 'views']
def get_comment_count(self, obj):
return obj.comments.count()
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError('Title too short.')
return value
ViewSets & Routers
# blog/views_api.py
from rest_framework import viewsets, permissions, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from .models import Post, Comment
from .serializers import PostSerializer, CommentSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.filter(status='published')
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
lookup_field = 'slug'
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
filterset_fields = ['category', 'author']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'views', 'title']
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def increment_view(self, request, slug=None):
post = self.get_object()
post.views += 1
post.save(update_fields=['views'])
return Response({'views': post.views})
URL Routing
# blog/urls.py
from rest_framework.routers import DefaultRouter
from .views_api import PostViewSet
router = DefaultRouter()
router.register(r'posts', PostViewSet)
urlpatterns = [
# ... existing URL patterns
path('api/', include(router.urls)),
]
This automatically generates all endpoints:
GET /api/posts/ # List all posts
POST /api/posts/ # Create a post (auth required)
GET /api/posts/{slug}/ # Retrieve a post
PUT /api/posts/{slug}/ # Update a post (author only)
DELETE /api/posts/{slug}/ # Delete a post (author only)
POST /api/posts/{slug}/increment_view/ # Custom action
💡 Did You Know? Django REST Framework comes with a browsable API. Just visit
http://127.0.0.1:8000/api/posts/in a browser, and you get an interactive UI to test your API endpoints. No need for Postman for basic testing.
🧪 Testing in Django
Django makes testing a first-class citizen. It provides a test client, test database, and assertion helpers out of the box.
Writing Tests
# blog/tests.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from .models import Post, Category
class PostModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.category = Category.objects.create(name='Python')
self.post = Post.objects.create(
title='Test Post',
content='Content for testing',
author=self.user,
category=self.category,
status='published'
)
def test_post_creation(self):
self.assertEqual(self.post.title, 'Test Post')
self.assertEqual(self.post.author.username, 'testuser')
self.assertTrue(self.post.slug) # Auto-generated
def test_post_str(self):
self.assertEqual(str(self.post), 'Test Post')
def test_published_manager(self):
published = Post.objects.filter(status='published')
self.assertEqual(published.count(), 1)
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='Test Post',
content='Content',
author=self.user,
status='published'
)
def test_post_list_view(self):
response = self.client.get(reverse('post-list'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Test Post')
self.assertTemplateUsed(response, 'blog/post_list.html')
def test_post_detail_view(self):
response = self.client.get(
reverse('post-detail', kwargs={'slug': self.post.slug})
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Test Post')
def test_authenticated_create_post(self):
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('post-create'), {
'title': 'New Post Title',
'content': 'Some content here',
'status': 'draft',
})
self.assertEqual(response.status_code, 302) # Redirect on success
self.assertEqual(Post.objects.count(), 2)
Running Tests
# Run all tests python manage.py test # Run specific app tests python manage.py test blog # Run with verbose output python manage.py test --verbosity=2 # Run with coverage (install django-coverage-plugin) coverage run manage.py test coverage report coverage html # Open htmlcov/index.html
🚀 Deployment & Production Best Practices
Deploying Django requires specific configuration. Here's the battle-tested approach.
Production Settings
# settings.py — Production overrides
import os
from pathlib import Path
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
# Use environment variables for secrets
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
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'),
}
}
# Security settings
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = 'DENY'
# Static files — collect to a single directory
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
Deployment with Gunicorn + Nginx
# Install gunicorn
pip install gunicorn
# Run with gunicorn
gunicorn myproject.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 4 \
--worker-class sync \
--timeout 30 \
--access-logfile /var/log/gunicorn/access.log \
--error-logfile /var/log/gunicorn/error.log
# /etc/nginx/sites-available/myproject
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$server_name$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;
location /static/ {
alias /path/to/staticfiles/;
expires 365d;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /path/to/media/;
expires 30d;
}
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}
Docker Deployment
# Dockerfile FROM python:3.12-slim WORKDIR /app ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN python manage.py collectstatic --noinput EXPOSE 8000 CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]
# docker-compose.yml
version: '3.8'
services:
db:
image: postgres:16
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: myproject
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
web:
build: .
command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 4
volumes:
- static_volume:/app/staticfiles
depends_on:
- db
environment:
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY}
DB_NAME: myproject
DB_USER: myuser
DB_PASSWORD: mypassword
DB_HOST: db
nginx:
image: nginx:latest
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- static_volume:/static
ports:
- "80:80"
- "443:443"
depends_on:
- web
volumes:
postgres_data:
static_volume:
⚡ Advanced Django Topics
Celery + Redis for Async Tasks
# celery.py (in project package)
import os
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
# tasks.py
from celery import shared_task
from django.core.mail import send_mail
@shared_task
def send_email_task(subject, message, recipient_list):
send_mail(subject, message, 'noreply@blog.com', recipient_list)
# In views — call asynchronously
send_email_task.delay('Subject', 'Body', ['user@example.com'])
Caching
# settings.py
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
}
}
# In views — cache expensive queries
from django.core.cache import cache
def get_popular_posts():
posts = cache.get('popular_posts')
if posts is None:
posts = Post.objects.filter(status='published')\
.annotate(comment_count=Count('comments'))\
.order_by('-views', '-comment_count')[:10]
cache.set('popular_posts', posts, 300) # 5 minutes
return posts
Custom Management Commands
# blog/management/commands/cleanup_drafts.py
from django.core.management.base import BaseCommand
from django.utils import timezone
from datetime import timedelta
from blog.models import Post
class Command(BaseCommand):
help = 'Delete draft posts older than 30 days'
def add_arguments(self, parser):
parser.add_argument('--days', type=int, default=30)
def handle(self, *args, **options):
cutoff = timezone.now() - timedelta(days=options['days'])
deleted, _ = Post.objects.filter(
status='draft',
created_at__lt=cutoff
).delete()
self.stdout.write(
self.style.SUCCESS(f'Cleaned up {deleted} old drafts')
)
# Run with: python manage.py cleanup_drafts --days 60
🏗️ Real-World Project: Build a Full Blog Engine
Let's build something real — a production-ready blog engine with auth, comments, API, and caching. Same architecture used by real Django projects.
Step 1: Project Setup
mkdir django_blog cd django_blog python3 -m venv venv source venv/bin/activate pip install django djangorestframework django-filter celery[redis] psycopg2-binary gunicorn django-admin startproject config . python manage.py startapp blog python manage.py startapp accounts
Step 2: Database Models
# blog/models.py (as shown in Models section above) # Then run: python manage.py makemigrations python manage.py migrate python manage.py createsuperuser
Step 3: Build Features
# Create views, templates, forms for: # 1. Post listing with pagination # 2. Post detail with comments # 3. Create/Update/Delete posts (author only) # 4. User registration & login # 5. Search by title/content # 6. Category filtering # 7. RSS feed # 8. Sitemap # 9. REST API endpoints # 10. Admin customization # After everything: python manage.py collectstatic python manage.py test blog --verbosity=2
Step 4: Deploy
# Push to GitHub, deploy with Docker git init git add . git commit -m "Full blog engine" git remote add origin https://github.com/yourusername/django-blog.git git push -u origin main # On server: git clone https://github.com/yourusername/django-blog.git cd django-blog docker-compose up -d --build
❌ Common Mistakes & How to Avoid Them
🔴 Mistake #1: Putting Business Logic in Views
What happens: Views become 200-line monsters. Testing becomes impossible. Reuse is zero.
How to fix: Follow the Fat Models, Thin Views pattern. Move business logic to model methods or service layers.
# Wrong — logic in view
def publish_post(request, slug):
post = get_object_or_404(Post, slug=slug)
if request.user == post.author:
post.status = 'published'
post.published_at = timezone.now()
post.save()
send_mail(...)
# Right — logic in model
class Post(models.Model):
def publish(self):
self.status = 'published'
self.published_at = timezone.now()
self.save()
send_publish_notification.delay(self.id)
# View becomes 2 lines:
def publish_post(request, slug):
post = get_object_or_404(Post, slug=slug, author=request.user)
post.publish()
🔴 Mistake #2: N+1 Query Problem
What happens: Fetching 100 posts with authors triggers 101 database queries (1 for posts + 100 for authors).
How to fix: Use select_related() for FK/O2O and prefetch_related() for M2M/reverse.
# Wrong — N+1 queries
posts = Post.objects.filter(status='published')
for post in posts:
print(post.author.username) # 1 query per post!
# Right — 1 query with JOIN
posts = Post.objects.select_related('author', 'category').filter(status='published')
🔴 Mistake #3: Storing Secrets in Code
What happens: SECRET_KEY, DB passwords end up in Git. Bad actors find them and compromise your site.
How to fix: Use environment variables or a .env file with python-decouple.
# Wrong
SECRET_KEY = 'django-insecure-abc123...'
DATABASES = {'default': {'PASSWORD': 'mypassword123'}}
# Right — env variables
import os
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
DB_PASSWORD = os.environ.get('DB_PASSWORD')
🔴 Mistake #4: NOT Using Database Indexes
What happens: Queries on large tables become slow as data grows (sequential scan instead of index scan).
How to fix: Add db_index=True on frequently filtered fields or use class Meta: indexes = [...].
🔴 Mistake #5: Overusing Generic Foreign Keys
What happens: Loss of referential integrity, complex queries, performance issues at scale.
How to fix: Use proper ForeignKey, ManyToManyField, or abstract base classes instead.
🔴 Mistake #6: DEBUG=True in Production
What happens: Detailed error pages expose your source code, DB schema, and secrets to anyone who triggers an error.
How to fix: Load DEBUG from env, default to False. Validate in production.
🔴 Mistake #7: Ignoring Migrations Conflicts
What happens: Two devs create migrations with the same number, causing chaos in CI/CD.
How to fix: Run makemigrations --merge and never edit existing migration files.
🧠 Test Your Knowledge
Before moving on, check if you really understood the key concepts:
- Which Django command creates database tables from your models?
A)python manage.py makemigrations
B)python manage.py migrate
C)python manage.py sqlmigrate
D)python manage.py createsuperuser
Answer: B —makemigrationscreates migration files,migrateapplies them to the database. - How do you prevent the N+1 query problem in Django ORM?
A) Use raw SQL queries
B) Useselect_related()andprefetch_related()
C) Disable lazy loading
D) Add more database indexes
Answer: B — These methods prefetch related objects in as few queries as possible. - What does
@login_requireddecorator do?
A) Encrypts the response
B) Requires the user to be authenticated to access the view
C) Logs all request data
D) Sends a login email
Answer: B — Redirects unauthenticated users to the login page. - Which Django REST Framework component converts Python objects to JSON?
A) ViewSets
B) Serializers
C) Routers
D) Filters
Answer: B — Serializers handle both serialization and deserialization of complex data types. - What is the correct way to increment a field without race conditions?
A)post.views += 1; post.save()
B)Post.objects.update(views=F('views') + 1)
C)post.views.count()
D)post.update(views__inc=1)
Answer: B — UsingF()expressions does the update atomically at the database level.
✅ Scored 4/5 or higher? You're ready to build real Django applications.
📉 Scored lower? Review the sections above before continuing.
❓ Frequently Asked Questions (FAQ)
Q1: Is Django still relevant in 2026 with all these new frameworks?
Absolutely. Django powers Instagram (500M+ daily users), and its job market grows 10% year over year. While new frameworks come and go, Django's maturity, ecosystem, and stability make it the #1 choice for Python web development. The latest Django 5.x series adds async support, modern ORM features, and better performance. It's not going anywhere.
Q2: Should I use Django or Flask for my project?
Use Django if you want the framework to handle auth, admin, ORM, forms, and security for you — and you're building a full-featured web app. Use Flask if you need a lightweight microframework for microservices, tiny APIs, or projects where you want to choose every component yourself. For most commercial projects, Django starts faster because you don't have to wire up 15 libraries.
Q3: How long does it take to learn Django?
You can build a basic CRUD app in 2-3 days with basic Python knowledge. Mastering the framework (ORM optimization, security, deployment, advanced patterns) takes 3-6 months of consistent practice. The learning curve is gentler than many frameworks because Django's documentation is excellent and the community is huge.
Q4: Can Django handle high traffic?
Yes. Instagram runs Django for 500M+ daily active users. Pinterest uses it for 450M+ monthly users. The key is proper architecture: caching (Redis/Memcached), database optimization (indexes, connection pooling), CDN for static files, and horizontal scaling with load balancers. Django itself handles concurrency well with proper configuration.
Q5: What database should I use with Django?
Start with SQLite for development — it's zero-config and ships with Python. Move to PostgreSQL for production. PostgreSQL is the recommended production database for Django: it supports all Django features (JSON fields, full-text search, array fields, etc.) and performs excellently at scale. MySQL/MariaDB work too but have feature gaps.
Q6: Is Django good for REST APIs?
Excellent. Django REST Framework (DRF) is the most popular Django package with 28K+ GitHub stars. It provides serializers, authentication classes, permissions, throttling, versioning, and a browsable API. For GraphQL, there's Graphene-Django. Django + DRF is the most popular Python API stack in production.
Q7: How do I handle file uploads in Django?
Django's FileField and ImageField handle uploads automatically. Files are stored in MEDIA_ROOT. For production, use django-storages with S3/GCS/Azure for scalable file storage. Always validate file types and sizes, and never serve user-uploaded files from your application server — use a CDN or object storage.
Q8: What's the best way to learn Django in 2026?
Start with the official Django tutorial (it's excellent). Then build a real project — clone a blog, a todo app, or a simple e-commerce site. Read Two Scoops of Django (the best Django book). Follow the Django subreddit and Django News newsletter. Contribute to open-source Django projects. The key is building real things, not just following tutorials.
Q9: Does Django support WebSockets?
Yes. Django Channels adds WebSocket, HTTP2, and async protocol support to Django. You define consumers (like views but for WebSockets) and use channel layers (Redis) for real-time features like chat, notifications, and live updates. Django 3.0+ also has native async views.
Q10: How do I secure my Django application?
Django is secure by default with CSRF protection, XSS prevention (auto-escaping in templates), SQL injection protection (parameterized queries), clickjacking protection (X-Frame-Options), and secure password hashing. Additional steps: use HTTPS everywhere, set DEBUG=False, keep dependencies updated, use environment variables for secrets, add rate limiting, validate all user input, and run security scans with bandit or django-security-checkup.
📖 Glossary: Key Terms Explained
| Term | Definition |
|---|---|
| ORM | Object-Relational Mapper — translates Python classes to database tables and queries to Python code |
| Migration | A file that describes changes to your database schema (create table, add column, etc.) |
| WSGI | Web Server Gateway Interface — standard for serving Python web applications synchronously |
| ASGI | Asynchronous Server Gateway Interface — modern standard for async Python web apps (WebSocket support) |
| CSRF | Cross-Site Request Forgery — attack where malicious site makes authenticated requests on your behalf |
| MVT | Model-View-Template — Django's architectural pattern (Model=DB, View=logic, Template=presentation) |
| Gunicorn | Green Unicorn — production WSGI HTTP server for Python web applications |
| DRF | Django REST Framework — toolkit for building REST APIs with Django |
| Celery | Distributed task queue for handling async/background jobs in Python |
| Middleware | A hook into Django's request/response processing chain (runs before and after views) |
| Signal | A notification system for when certain actions happen (e.g., "user just registered") |
| QuerySet | A lazy collection of database queries — filters chain together without hitting the DB until evaluated |
| ViewSet | DRF concept — a single class that handles all CRUD operations for a model (list, create, retrieve, update, delete) |
| F Expression | Allows atomic database operations without race conditions (e.g., incrementing a counter) |
| South | (Historical) Django's original migration system before migrations were built into Django 1.7+ |
✅ Do's & Don'ts: Quick Reference
| ✅ Do | ❌ Don't |
|---|---|
| Use environment variables for secrets | Hardcode SECRET_KEY or DB passwords in settings.py |
| Use select_related() and prefetch_related() | Ignore the N+1 query problem |
| Use model methods for business logic | Put 200 lines of logic in views.py |
| Use PostgreSQL for production | Use SQLite in production |
| Write tests for every model and view | Deploy without running tests |
| Use Redis/Memcached for caching | Hit the database on every single request |
| Use class-based views for complex views | Use FBVs when CBVs would be cleaner |
💡 10 Pro Tips Learned the Hard Way
- Start with a monolith. Don't split into microservices until you actually need to. Django's app structure already gives you modularity. Instagram ran as a single Django monolith for years.
- Use django-debug-toolbar in development. It shows you every SQL query, template context, and request timing. Catches N+1 problems immediately. Install with
pip install django-debug-toolbar. - Custom user model from day one. Even if you think you'll never need it. Use
AbstractUserinstead of the default User. Changing it later requires a database migration nightmare.python manage.py startapp accounts+AUTH_USER_MODEL = 'accounts.User'. - Use F() expressions for counters.
Post.objects.filter(id=post.id).update(views=F('views') + 1)is atomic.post.views += 1; post.save()has a race condition that will corrupt your data under load. - Database indexes are free for reads. Add
db_index=Trueor useclass Meta: indexeson every field you filter, sort, or join on. The write performance cost is negligible. The read performance gain is massive. - Use django-storages for file uploads. Never store user-uploaded files on the application server. Use S3, GCS, or DigitalOcean Spaces. It's cheaper, faster with CDN, and survives server restarts.
- Version your API. Use URL-based versioning (
/api/v1/posts/,/api/v2/posts/). It's the simplest and most explicit. Your mobile app will thank you when you need to make breaking changes. - Set up CI/CD early. GitHub Actions runs your tests on every push. Add linting (flake8/ruff), type checking (mypy), and security scanning (bandit) to your pipeline. Catches 90% of bugs before they reach production.
- Use connection pooling in production. PostgreSQL can only handle ~100 concurrent connections by default. Use PgBouncer or configure Django's
CONN_MAX_AGEto reuse connections. Your database will thank you. - Read the Django release notes. Each major version has deprecation warnings and new features. Staying current saves you from painful migrations later. Django 5.x deprecates things from Django 3.x — that's a 4-year window to update.
🗺️ Learning Roadmap: From Zero to Production in 7 Days
| Day | Topic | Goal | ⏱️ Time |
|---|---|---|---|
| 1 | Setup & First App | Install Django, create project, first hello world view | 60 min |
| 2 | Models & Database | Define models, run migrations, query with ORM | 90 min |
| 3 | Views, Templates & Admin | FBVs, CBVs, template inheritance, admin customization | 120 min |
| 4 | Forms, Auth & User Management | Login/logout, registration, permissions, form validation | 120 min |
| 5 | REST APIs with DRF | Serializers, ViewSets, permissions, API documentation | 120 min |
| 6 | Testing & Security | Write tests, security best practices, debug with toolbar | 90 min |
| 7 | Deploy to Production | Gunicorn, Nginx, Docker, PostgreSQL, CI/CD | 180 min |
🔍 Troubleshooting: Fix Common Problems
| ⚠️ Problem | 🔍 Cause | ✅ Solution |
|---|---|---|
| DisallowedHost error | Domain not in ALLOWED_HOSTS |
Add your domain to ALLOWED_HOSTS |
No module named 'blog' |
App not registered in INSTALLED_APPS |
Add 'blog' to INSTALLED_APPS |
| 403 Forbidden on POST | Missing CSRF token | Add {% csrf_token %} inside your form |
| Static files 404 in production | collectstatic not run or Nginx misconfigured |
Run collectstatic, verify Nginx static alias |
| Database migration conflicts | Two devs created migrations with same dependency | Run makemigrations --merge |
| Slow page loads | N+1 queries, no caching, missing indexes | Add select_related, caching, db indexes |
OperationalError: too many connections |
Database connection exhaustion | Set CONN_MAX_AGE or use PgBouncer |
💬 What's Your Experience?
Have you built anything with Django? What was the hardest concept to grasp? Drop a comment below — I read and reply to every single one.
Quick questions to start the conversation:
- What's the one thing you wish someone told you when you started Django?
- Django vs FastAPI — what's your preference and why?
- Which section of this course was most helpful?
- What should I cover in the advanced Django course?
📌 TL;DR: If You Learn Nothing Else, Learn These 5
- MVT Architecture — Models define data, Views handle logic, Templates render HTML. Keep them separate.
- Django ORM —
select_related(),prefetch_related(),F()expressions, andQ()objects are your power tools. - CBVs — Class-Based Views with mixins give you auth, pagination, and CRUD in 5 lines of code.
- Security — Django handles CSRF, XSS, and SQL injection by default. Never override that protection.
- Deployment — Gunicorn + Nginx + PostgreSQL + Redis + Docker. That's your production stack.
More Free Courses on TricksPage
- Git & GitHub Course — Master Git from basics to collaboration workflows, CI/CD, and open-source.
- Linux Commands Course — Complete Linux command line mastery.
- Docker & Swarm Course — Containers, Dockerfiles, Compose, Swarm orchestration.
- n8n Automation Course — Workflow automation with 400+ integrations.
- Agentic AI Course — Build AI agents with ReAct patterns, tools, memory.
💭 Final Thoughts
Django is more than just a web framework — it's a philosophy. The "batteries-included" approach means you spend your time building features, not choosing between 50 different authentication libraries. It's why Instagram, Pinterest, and Spotify trust it with millions of users.
The learning curve isn't steep — it's wide. There's a lot to learn (just look at this course), but none of it is hard once you understand the patterns. Models define your data. Views handle your logic. Templates render your HTML. Everything else is built on that foundation.
🔥 Final Word: The fastest way to learn Django is to build something real. Not a todo app. Not a tutorial clone. Something you'd actually use. That's where the real learning happens.
The best time to start was yesterday. The second best time is now. 🚀
More Free Courses on TricksPage
- Git & GitHub Course — Master Git from basics to collaboration workflows, CI/CD, and open-source.
- Linux Commands Course — Complete Linux command line mastery.
- Docker & Swarm Course — Containers, Dockerfiles, Compose, Swarm orchestration.
- n8n Automation Course — Workflow automation with 400+ integrations.
- Agentic AI Course — Build AI agents with ReAct patterns, tools, memory.
If this course helped you:
- 📌 Bookmark this page for future reference
- 📤 Share it with someone who needs it
- 💬 Leave a comment — I read every one
- ⭐ Follow the blog for more deep courses