Django: Building Python Web Apps
Django is Python's most battle-tested, full-featured web framework — powering Instagram, Pinterest, Disqus, and NASA's web properties at massive scale. If you've worked with Flask or FastAPI and found yourself repeatedly building the same scaffolding (authentication, admin panels, ORM, form validation), Django eliminates all of that boilerplate by including it out of the box. This guide takes you from zero to a working Django application, explaining the why behind every major concept along the way.
Flask and FastAPI are excellent micro-frameworks that give you complete control — but you assemble the pieces yourself. Django's "batteries-included" philosophy ships with an ORM, admin interface, authentication system, form handling, migrations, and security middleware pre-integrated. For data-driven web applications, Django typically cuts development time in half.
The MVT Architecture
Django follows the Model-View-Template (MVT) pattern — a slight variation of MVC where Django's framework itself acts as the Controller. Understanding this separation is essential before writing your first line of Django code.
- Model: Defines your data structure and handles all database interactions via Django's ORM. One Python class = one database table.
- View: Contains your business logic. Views receive HTTP requests, query the database through models, and return HTTP responses.
- Template: HTML files with Django's templating language for rendering dynamic content. Templates receive context data from views and produce the final HTML.
Django's MVT architecture: URL Router dispatches requests to Views, which interact with Models and render Templates
Installation and Project Setup
Django requires Python 3.10 or higher. Always use a virtual environment to isolate project dependencies — this prevents version conflicts across projects.
| Requirement | Minimum | Recommended |
|---|---|---|
| Python | 3.10 | 3.12+ |
| Django | 4.2 (LTS) | 5.1+ |
| Database | SQLite (dev only) | PostgreSQL 14+ |
| pip | 22.0 | Latest |
Create a Virtual Environment
Isolate your project dependencies using Python's built-in venv module or uv for faster installs.
# Create and activate virtual environment
python -m venv venv
source venv/bin/activate # Linux/macOS
# venv\Scripts\activate # Windows
# Install Django
pip install django
# Verify installation
python -m django --version # Should print 5.x.x
Create a Django Project
The startproject command scaffolds the top-level configuration files for your Django site.
# Create the project (trailing dot avoids a nested folder)
django-admin startproject mysite .
# Run the development server
python manage.py runserver
# → http://127.0.0.1:8000/
Create Your First App
Django projects are composed of reusable apps. Each app has its own models, views, and URLs — keeping the codebase modular.
# Create a "blog" app inside your project
python manage.py startapp blog
Project Structure Explained
After running these commands, your project layout looks like this. Understanding each file's role saves hours of confusion later:
mysite/ ← project config package
__init__.py
settings.py ← all project settings (DB, INSTALLED_APPS, etc.)
urls.py ← root URL configuration
wsgi.py ← WSGI entry point for production servers
asgi.py ← ASGI entry point for async servers
blog/ ← your first app
migrations/ ← auto-generated database migration files
__init__.py
admin.py ← register models with Django admin
apps.py ← app configuration class
models.py ← database model definitions
tests.py ← unit and integration tests
views.py ← request handlers (business logic)
urls.py ← (you create this) app-level URL routing
manage.py ← CLI tool for running dev server, migrations, etc.
Register the App in settings.py
Django won't discover your new app until you add it to INSTALLED_APPS in mysite/settings.py.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog', # ← add your app here
]
Models and the ORM
Django's ORM (Object-Relational Mapper) lets you define your database schema using Python classes. Each class subclassing models.Model maps to a database table, and each class attribute maps to a column. You never have to write raw SQL for standard CRUD operations.
Defining Models
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
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=250)
slug = models.SlugField(max_length=250, unique_for_date='publish')
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
body = models.TextField()
publish = models.DateTimeField(default=timezone.now)
created = models.DateTimeField(auto_now_add=True) # set once on creation
updated = models.DateTimeField(auto_now=True) # updated on every save
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
class Meta:
ordering = ['-publish']
indexes = [models.Index(fields=['-publish'])]
def __str__(self):
return self.title
Running Migrations
Migrations are Django's version control for your database schema. After any change to models.py, you run two commands:
# Generate migration file from model changes
python manage.py makemigrations
# Apply pending migrations to the database
python manage.py migrate
# Inspect the SQL Django will run (without executing it)
python manage.py sqlmigrate blog 0001
Querying with the ORM
from blog.models import Post
from django.contrib.auth.models import User
# Retrieve all published posts (QuerySet is lazy — no DB hit yet)
published = Post.objects.filter(status='published')
# Chaining filters (AND logic)
recent = Post.objects.filter(
status='published',
publish__year=2026
).order_by('-publish')
# OR logic using Q objects
from django.db.models import Q
results = Post.objects.filter(
Q(title__icontains='django') | Q(body__icontains='django')
)
# Select related (JOIN — avoids N+1 queries)
posts = Post.objects.select_related('author', 'category').all()
# Aggregate: count posts per category
from django.db.models import Count
Post.objects.values('category__name').annotate(total=Count('id'))
# Create a new post
user = User.objects.get(username='admin')
post = Post.objects.create(
title='My First Post',
slug='my-first-post',
author=user,
body='Hello, Django!',
status='published',
)
# Update
Post.objects.filter(author=user).update(status='published')
# Delete
Post.objects.filter(status='draft', publish__lt=timezone.now() - timedelta(days=30)).delete()
The most common Django performance mistake is loading related objects in a loop. Use select_related() for ForeignKey/OneToOne relationships (generates a SQL JOIN) and prefetch_related() for ManyToMany and reverse FK relationships (runs a separate optimized query). Always profile with Django Debug Toolbar before optimizing.
Views and URL Routing
Django views are Python callables (functions or class methods) that receive an HttpRequest and return an HttpResponse. URL patterns in urls.py map URL paths to view functions using regular expressions or simpler path converters.
Function-Based Views (FBVs)
from django.shortcuts import render, get_object_or_404
from django.core.paginator import Paginator
from .models import Post
def post_list(request):
posts = Post.objects.filter(status='published')
paginator = Paginator(posts, 10) # 10 posts per page
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
return render(request, 'blog/post_list.html', {'page_obj': page_obj})
def post_detail(request, year, month, day, slug):
post = get_object_or_404(
Post,
status='published',
slug=slug,
publish__year=year,
publish__month=month,
publish__day=day,
)
return render(request, 'blog/post_detail.html', {'post': post})
Class-Based Views (CBVs)
Class-based views reduce boilerplate for common patterns like displaying lists, detail pages, and forms. Django's generic CBVs handle pagination, context, and queryset filtering with minimal code:
from django.views.generic import ListView, DetailView
from .models import Post
class PostListView(ListView):
model = Post
queryset = Post.objects.filter(status='published')
context_object_name = 'posts'
paginate_by = 10
template_name = 'blog/post_list.html'
class PostDetailView(DetailView):
model = Post
context_object_name = 'post'
template_name = 'blog/post_detail.html'
def get_queryset(self):
return Post.objects.filter(status='published')
URL Configuration
from django.urls import path
from . import views
app_name = 'blog' # namespace for URL reversing
urlpatterns = [
path('', views.PostListView.as_view(), name='post_list'),
path(
'<int:year>/<int:month>/<int:day>/<slug:slug>/',
views.PostDetailView.as_view(),
name='post_detail',
),
]
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls', namespace='blog')),
]
Django Templates
Django's template language is intentionally simple — it's designed for HTML authors, not programmers. The key feature is template inheritance: define a base layout once and extend it in child templates, keeping navigation, headers, and footers DRY.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}My Blog{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
{% load static %}
</head>
<body>
<nav>...</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer>...</footer>
</body>
</html>
{% extends 'base.html' %}
{% block title %}Latest Posts | My Blog{% endblock %}
{% block content %}
<h1>Latest Posts</h1>
{% for post in page_obj %}
<article>
<h2>
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
</h2>
<p>By {{ post.author.get_full_name }} on {{ post.publish|date:"F j, Y" }}</p>
<p>{{ post.body|truncatewords:30 }}</p>
</article>
{% empty %}
<p>No posts published yet.</p>
{% endfor %}
{# Pagination #}
{% if page_obj.has_other_pages %}
<nav>
{% 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 %}
Django Admin Interface
One of Django's most powerful features is the automatic admin interface. Register your models and get a fully functional CRUD interface immediately — no frontend code required. This is genuinely useful in production for content management and data inspection.
from django.contrib import admin
from .models import Category, Post
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug']
prepopulated_fields = {'slug': ('name',)}
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'status', 'publish']
list_filter = ['status', 'created', 'publish', 'author']
search_fields = ['title', 'body']
prepopulated_fields = {'slug': ('title',)}
raw_id_fields = ['author'] # search widget instead of dropdown
date_hierarchy = 'publish'
ordering = ['status', '-publish']
list_editable = ['status'] # edit status inline in the list view
python manage.py createsuperuser
# → Visit http://127.0.0.1:8000/admin/ to log in
Authentication and Security
Django ships with a complete authentication system: user accounts, password hashing (PBKDF2 by default), login/logout views, password reset via email, and permission-based access control. You rarely need a third-party library for standard auth.
from django.contrib.auth import views as auth_views
urlpatterns += [
# Built-in login/logout/password reset views
path('accounts/', include('django.contrib.auth.urls')),
]
# Provides: /accounts/login/ /accounts/logout/
# /accounts/password_change/ /accounts/password_reset/
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
# Function-based view protection
@login_required
def create_post(request):
...
# Class-based view protection
class CreatePostView(LoginRequiredMixin, CreateView):
model = Post
fields = ['title', 'body', 'status']
# LoginRequiredMixin redirects unauthenticated users to settings.LOGIN_URL
Before deploying, set DEBUG = False in settings, configure ALLOWED_HOSTS, use environment variables for SECRET_KEY and database credentials (never commit them to git), serve static files via a CDN or Nginx, and enable HTTPS. Run python manage.py check --deploy to get Django's built-in security audit.
Django's Built-in Security Features
- CSRF Protection: Every POST form requires a
{% csrf_token %}tag. Django validates it automatically — no configuration needed. - SQL Injection Protection: The ORM parameterizes all queries. Avoid
raw()with user input unless you manually escape. - XSS Prevention: The template engine auto-escapes HTML special characters. Use
{{ value|safe }}only when you trust the source. - Clickjacking Protection: The
X-Frame-Options: DENYheader is sent by default viaXFrameOptionsMiddleware. - Secure Cookies: Set
SESSION_COOKIE_SECURE = TrueandCSRF_COOKIE_SECURE = Truein production to restrict cookies to HTTPS.
Deploying a Django Application
Django ships with a development server (runserver) that is not suitable for production. The standard production stack uses Gunicorn as the WSGI application server behind Nginx as a reverse proxy.
| Component | Role | Alternative |
|---|---|---|
| Gunicorn | WSGI/ASGI server — runs Python code | uWSGI, Daphne (async) |
| Nginx | Reverse proxy, static files, SSL termination | Caddy, Apache |
| PostgreSQL | Production database | MySQL, MariaDB |
| Redis | Cache backend, session storage, Celery broker | Memcached |
| Celery | Asynchronous task queue | Django Q, Dramatiq |
pip install gunicorn psycopg2-binary whitenoise
# Collect static files into STATIC_ROOT
python manage.py collectstatic
# Run with 4 worker processes
gunicorn mysite.wsgi:application --workers 4 --bind 0.0.0.0:8000
import os
SECRET_KEY = os.environ['DJANGO_SECRET_KEY'] # never hardcode!
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
# PostgreSQL
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ['DB_NAME'],
'USER': os.environ['DB_USER'],
'PASSWORD': os.environ['DB_PASSWORD'],
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': '5432',
}
}
# WhiteNoise serves static files efficiently without Nginx
MIDDLEWARE = ['whitenoise.middleware.WhiteNoiseMiddleware', ...rest...]
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Redis cache
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': os.environ.get('REDIS_URL', 'redis://127.0.0.1:6379/1'),
}
}
# Security headers
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
Conclusion
Django's "batteries-included" philosophy means you spend far less time assembling infrastructure and far more time building the features that make your application unique. The ORM eliminates 90% of raw SQL, the admin interface handles internal tooling out of the box, the auth system is production-hardened, and the migration engine keeps your schema in sync with your code across every environment.
The path forward from here is rich: explore Django REST Framework (DRF) for building APIs consumed by React or mobile apps, Django Channels for WebSocket-powered real-time features, Celery for background job processing, and django-allauth for social authentication. Each library integrates naturally with Django's architecture because it was designed for exactly this kind of ecosystem growth.
Key Takeaways
- Django's MVT pattern cleanly separates data (Model), logic (View), and presentation (Template)
- The ORM handles migrations, CRUD, and complex queries — use
select_related()to avoid N+1 problems - URL namespacing with
app_nameand{% url %}tags keeps links refactor-safe - Template inheritance via
{% extends %}keeps your HTML DRY across all pages - The built-in admin, auth, and CSRF/XSS protection give you a production-ready security baseline for free
- Deploy with Gunicorn + Nginx, PostgreSQL, and always externalize secrets via environment variables