What is Django?
Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. Built by experienced developers, it takes care of much of the hassle of web development so you can focus on writing your application without reinventing the wheel. Django follows the "batteries-included" philosophy — it ships with an ORM, authentication system, admin interface, form handling, template engine, and many other components right out of the box.
At its core, Django is built around the MVT (Model-View-Template) architectural pattern:
- Model — Defines your data schema as Python classes. Each model maps to a single database table, and Django's ORM handles all SQL operations automatically.
- View — Contains the business logic. Views receive HTTP requests, interact with models, and return HTTP responses. They are the bridge between models and templates.
- Template — Handles the presentation layer. Django's template language allows you to write dynamic HTML with variables, filters, and tags.
Why Django Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Django powers some of the busiest sites on the internet — Instagram, Pinterest, Mozilla, and The Washington Post all use Django in their stacks. Here's why it deserves your attention:
- Speed of development — Django's built-in components eliminate repetitive boilerplate. You can go from concept to a functional prototype in hours, not days.
- Security-first approach — Django automatically protects against SQL injection, CSRF, XSS, and clickjacking attacks by default. You don't need to remember to add protection — it's already there.
- Scalability proven in production — From small side projects to applications serving millions of users, Django scales horizontally with ease using standard load-balancing and caching strategies.
- Massive ecosystem — Thousands of third-party packages are available via pip, covering everything from REST APIs (Django REST Framework) to real-time features (Django Channels) and CMS solutions (Wagtail).
- Excellent documentation and community — Django's official documentation is widely regarded as one of the best in open source, and the community provides abundant tutorials, forums, and conferences.
Prerequisites
Before diving in, ensure you have:
- Python 3.8 or later installed on your system
- A command-line terminal you're comfortable with
- A code editor (VS Code, PyCharm, or Sublime Text)
- Basic understanding of Python, HTTP, and HTML
Step 1: Setting Up Your Environment
Best practice is to create an isolated virtual environment for each Django project. This prevents dependency conflicts between projects.
# Create a project directory and navigate into it
mkdir django_blog && cd django_blog
# Create a virtual environment (use python3 if on macOS/Linux)
python -m venv venv
# Activate the virtual environment
# On Windows:
venv\Scripts\activate
# On macOS/Linux:
source venv/bin/activate
# Install Django
pip install django
# Verify the installation
python -m django --version
# Output should show something like: 4.2.7
Step 2: Creating Your First Django Project
A Django project is the top-level container that holds all settings, URL configurations, and applications. Use the django-admin command to scaffold it:
# Create the project named 'myblog'
django-admin startproject myblog .
# The trailing dot places manage.py in the current directory.
# Project structure:
# django_blog/
# ├── manage.py # Command-line utility for admin tasks
# ├── venv/ # Virtual environment (not tracked in git)
# └── myblog/
# ├── __init__.py # Marks this as a Python package
# ├── settings.py # Project configuration
# ├── urls.py # Root URL routing table
# ├── asgi.py # ASGI entry point for async servers
# └── wsgi.py # WSGI entry point for deployment
Let's examine the key files you just created:
- manage.py — A command-line tool that wraps
django-adminwith your project's settings automatically configured. You'll use it for running the development server, making migrations, and more. - settings.py — The central configuration file. Contains database settings, installed apps, middleware, and static file paths.
- urls.py — The root URL dispatcher. Each incoming request is matched against URL patterns defined here and routed to the appropriate view.
Step 3: Your First Run — The Development Server
Django ships with a lightweight development server that auto-reloads when you change code. Start it to verify everything works:
python manage.py runserver
# Output:
# Watching for file changes with StatReloader
# Performing system checks...
# System check identified no issues.
# Django version 4.2.7, using settings 'myblog.settings'
# Starting development server at http://127.0.0.1:8000/
# Quit the server with CONTROL-C.
Open http://127.0.0.1:8000/ in your browser. You'll see Django's default welcome page with the rocket ship. This confirms your installation is working perfectly.
Step 4: Understanding Django Applications
A Django project is composed of multiple applications. Each app is a self-contained Python package that handles a specific domain of functionality. This modular design lets you plug apps into different projects or reuse them across projects.
Create your first app — we'll call it posts to handle blog articles:
python manage.py startapp posts
# New directory structure:
# posts/
# ├── __init__.py
# ├── admin.py # Configuration for Django's auto-generated admin interface
# ├── apps.py # App configuration class
# ├── migrations/ # Database migration files (auto-generated)
# │ └── __init__.py
# ├── models.py # Database models (your data schema)
# ├── tests.py # Test cases
# └── views.py # View functions/classes that handle HTTP requests
Now register the app in your project's settings. Open myblog/settings.py and add 'posts' to the INSTALLED_APPS list:
# myblog/settings.py (excerpt)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Add your app here
'posts',
]
Step 5: Defining Models — Your Database Schema
Models are Python classes that define your database tables. Django's ORM translates these classes into SQL automatically. Let's create a Post model for blog articles:
# posts/models.py
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
class Post(models.Model):
"""Represents a single blog post."""
# Fields
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True,
help_text="URL-friendly version of the title")
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='blog_posts'
)
body = models.TextField()
excerpt = models.TextField(max_length=500, blank=True,
help_text="Short summary for listing pages")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(default=timezone.now)
status = models.CharField(
max_length=10,
choices=[
('draft', 'Draft'),
('published', 'Published'),
],
default='draft'
)
class Meta:
ordering = ['-published_at'] # Newest posts first
indexes = [
models.Index(fields=['status', 'published_at']),
]
def __str__(self):
return self.title
def get_absolute_url(self):
"""Returns the canonical URL for this post."""
from django.urls import reverse
return reverse('post_detail', kwargs={'slug': self.slug})
Key model field types used here:
- CharField — Short text with a maximum length. Maps to VARCHAR in SQL.
- SlugField — A URL-safe string (lowercase letters, numbers, hyphens). Used for SEO-friendly URLs.
- ForeignKey — Defines a one-to-many relationship. Each post belongs to one author, but one author can have many posts.
- TextField — Arbitrary-length text. Maps to TEXT in SQL.
- DateTimeField — Stores dates and times.
auto_now_addsets the value once on creation;auto_nowupdates on every save. - choices — Restricts field values to a predefined set, stored efficiently in the database.
Step 6: Migrations — Syncing Models with the Database
Migrations are Django's version-control system for your database schema. They track changes to your models and generate the necessary SQL to keep the database in sync. Run these commands:
# Generate migration files based on your models.py changes
python manage.py makemigrations
# Output:
# Migrations for 'posts':
# posts/migrations/0001_initial.py
# - Create model Post
# Apply the migrations to your database (SQLite by default)
python manage.py migrate
# Output:
# Operations to perform:
# Apply all migrations: admin, auth, contenttypes, posts, sessions
# Running migrations:
# Applying posts.0001_initial... OK
# ... (other migrations)
Behind the scenes, Django created an SQLite database file (db.sqlite3) in your project root. You can inspect the generated SQL at any time:
python manage.py sqlmigrate posts 0001
Step 7: Django's Interactive Shell — Playing with Models
Django provides an enhanced Python shell with your project context pre-loaded. Let's create some data:
python manage.py shell
# Now inside the Django shell:
>>> from posts.models import Post
>>> from django.contrib.auth.models import User
>>> from django.utils import timezone
# Create a superuser first (we'll need one later for the admin)
# Exit shell and run: python manage.py createsuperuser
# Then come back to the shell
# Fetch an existing user (the superuser you just created)
>>> user = User.objects.first()
# Create a blog post
>>> post = Post.objects.create(
... title="Getting Started with Django",
... slug="getting-started-with-django",
... author=user,
... body="Django is a powerful Python web framework...",
... excerpt="Learn Django from scratch",
... status="published"
... )
# Query all published posts
>>> Post.objects.filter(status='published')
]>
# Get a single post by slug
>>> Post.objects.get(slug='getting-started-with-django')
# Update a post
>>> post.title = "Django for Beginners"
>>> post.save()
>>> post.title
'Django for Beginners'
Step 8: Creating Views — Handling HTTP Requests
Views are functions (or classes) that receive an HTTP request and return an HTTP response. Every view must either return a response object or raise an exception. Let's create views for listing posts and showing a single post:
# posts/views.py
from django.shortcuts import render, get_object_or_404
from django.views import View
from .models import Post
# Function-based view for listing published posts
def post_list(request):
"""Display a list of all published blog posts."""
posts = Post.objects.filter(
status='published'
).select_related('author').all()
context = {'posts': posts}
return render(request, 'posts/list.html', context)
# Class-based view for a single post detail
class PostDetailView(View):
"""Display a single blog post with full content."""
def get(self, request, slug):
post = get_object_or_404(
Post.objects.select_related('author'),
slug=slug,
status='published'
)
context = {'post': post}
return render(request, 'posts/detail.html', context)
Important points:
- render() — Combines a template with context data and returns an HttpResponse. This is the most common way to return HTML.
- get_object_or_404() — A shortcut that raises Http404 if the object doesn't exist. Prevents manual try/except blocks.
- select_related() — Performs a SQL JOIN to fetch the related author in the same query, avoiding the N+1 query problem.
Step 9: URL Routing — Mapping URLs to Views
URLs are dispatched through a hierarchical routing system. Each app gets its own urls.py file, which is included into the project's root URL configuration.
First, create posts/urls.py:
# posts/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.post_list, name='post_list'),
path('post/<slug:slug>/', views.PostDetailView.as_view(), name='post_detail'),
]
Then include it in the project's root URL configuration:
# myblog/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('posts.urls')), # Include the posts app URLs at root
]
The URL pattern <slug:slug> captures a slug from the URL and passes it as a keyword argument to the view. For example, /post/getting-started-with-django/ passes slug='getting-started-with-django' to the view.
Step 10: Templates — Rendering Dynamic HTML
Templates are HTML files with Django's template language sprinkled in. They support variables, filters, tags, template inheritance, and more.
Create the template directory structure inside your app:
mkdir -p posts/templates/posts
Now create a base template that all pages will inherit from:
# posts/templates/posts/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 Blog{% endblock %}</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 800px;
margin: 0 auto; padding: 20px; line-height: 1.6; }
nav { margin-bottom: 30px; border-bottom: 1px solid #ddd;
padding-bottom: 10px; }
nav a { margin-right: 15px; text-decoration: none; color: #0366d6; }
.post-item { margin-bottom: 25px; border-bottom: 1px solid #eee;
padding-bottom: 15px; }
.post-meta { color: #666; font-size: 0.9em; }
h1 a { text-decoration: none; color: #333; }
footer { margin-top: 40px; border-top: 1px solid #ddd;
padding-top: 15px; color: #888; font-size: 0.85em; }
</style>
</head>
<body>
<nav>
<a href="{% url 'post_list' %}">Home</a>
<a href="/admin/">Admin</a>
</nav>
{% block content %}
<!-- Page-specific content goes here -->
{% endblock %}
<footer>
<p>Built with Django. © {% now "Y" %} My Blog.</p>
</footer>
</body>
</html>
Create the post list template:
# posts/templates/posts/list.html
{% extends 'posts/base.html' %}
{% block title %}Blog Posts — My Blog{% endblock %}
{% block content %}
<h1>Blog Posts</h1>
{% if posts %}
{% for post in posts %}
<div class="post-item">
<h2>
<a href="{% url 'post_detail' slug=post.slug %}">
{{ post.title }}
</a>
</h2>
<p class="post-meta">
By {{ post.author.username }} on
{{ post.published_at|date:"F j, Y" }}
</p>
<p>{{ post.excerpt|default:post.body|truncatewords:50 }}</p>
</div>
{% endfor %}
{% else %}
<p>No posts have been published yet. Check back soon!</p>
{% endif %}
{% endblock %}
Create the post detail template:
# posts/templates/posts/detail.html
{% extends 'posts/base.html' %}
{% block title %}{{ post.title }} — My Blog{% endblock %}
{% block content %}
<article>
<h1>{{ post.title }}</h1>
<p class="post-meta">
By <strong>{{ post.author.username }}</strong>
Published on {{ post.published_at|date:"F j, Y" }}
{% if post.updated_at != post.published_at %}
— Updated {{ post.updated_at|date:"F j, Y" }}
{% endif %}
</p>
<div>
{{ post.body|linebreaks }}
</div>
<p style="margin-top: 30px;">
<a href="{% url 'post_list' %}">← Back to all posts</a>
</p>
</article>
{% endblock %}
Key template concepts demonstrated:
- {% extends %} — Template inheritance. The child template fills in blocks defined by the parent.
- {% block %}...{% endblock %} — Defines replaceable content areas.
- {% url %} — Reverses a named URL pattern into its actual path. Never hardcode URLs!
- {{ variable|filter }} — Applies filters like
date,truncatewords, andlinebreaks. - {% if %}...{% else %}...{% endif %} — Conditional rendering.
Now visit http://127.0.0.1:8000/ in your browser. You should see your published blog posts listed, and clicking a title takes you to the detail page.
Step 11: The Django Admin — Auto-Generated Interface
Django's admin interface is one of its most powerful features. It automatically generates a CRUD interface based on your models. Register your model in posts/admin.py:
# posts/admin.py
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
"""Configuration for the Post admin interface."""
# Columns displayed in the list view
list_display = ['title', 'author', 'status', 'published_at', 'created_at']
# Fields that become clickable links to the detail page
list_display_links = ['title']
# Filters displayed in the sidebar
list_filter = ['status', 'author', 'published_at']
# Search fields (searches these fields in the database)
search_fields = ['title', 'body', 'excerpt']
# Automatically populate slug from title
prepopulated_fields = {'slug': ('title',)}
# Fields displayed in the edit form, organized into sections
fieldsets = (
('Content', {
'fields': ('title', 'slug', 'body', 'excerpt')
}),
('Publication', {
'fields': ('author', 'status', 'published_at')
}),
)
# Raw ID fields for ForeignKey with many options
raw_id_fields = ['author']
Create a superuser to access the admin:
python manage.py createsuperuser
# Follow the prompts:
# Username: admin
# Email address: admin@example.com
# Password: [enter a strong password]
# Password (again): [repeat]
# Superuser created successfully.
Now visit http://127.0.0.1:8000/admin/, log in with your superuser credentials, and you'll see a fully functional admin interface where you can create, edit, and delete posts.
Step 12: Adding Forms for User Input
Django's form system handles rendering HTML forms, validating submitted data, and converting data to Python types. Let's add a comment feature to demonstrate forms.
First, extend posts/models.py with a Comment model:
# Add to posts/models.py
class Comment(models.Model):
"""User comment on a blog post."""
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.title}'
Create the migration and apply it:
python manage.py makemigrations
python manage.py migrate
Now create a form class. You can define forms in posts/forms.py:
# posts/forms.py
from django import forms
from .models import Comment
class CommentForm(forms.ModelForm):
"""Form for submitting comments on blog posts."""
class Meta:
model = Comment
fields = ['name', 'email', 'body']
widgets = {
'body': forms.Textarea(attrs={'rows': 5, 'cols': 40}),
}
labels = {
'body': 'Your Comment',
}
def clean_email(self):
"""Custom validation for the email field."""
email = self.cleaned_data.get('email')
if email and not '@' in email:
raise forms.ValidationError('Please enter a valid email address.')
return email
Step 13: Processing Forms in Views
Update posts/views.py to handle comment submissions on the detail page:
# Updated PostDetailView in posts/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.views import View
from django.urls import reverse
from .models import Post, Comment
from .forms import CommentForm
class PostDetailView(View):
"""Display a single blog post and handle comment submissions."""
def get(self, request, slug):
post = get_object_or_404(
Post.objects.select_related('author'),
slug=slug,
status='published'
)
comments = post.comments.filter(active=True)
form = CommentForm()
context = {
'post': post,
'comments': comments,
'form': form,
}
return render(request, 'posts/detail.html', context)
def post(self, request, slug):
post = get_object_or_404(Post, slug=slug, status='published')
form = CommentForm(request.POST)
if form.is_valid():
# Create the comment but don't save to DB yet
comment = form.save(commit=False)
comment.post = post # Assign the post relationship
comment.save()
return redirect(
reverse('post_detail', kwargs={'slug': post.slug})
)
# Form is invalid, re-render with errors
comments = post.comments.filter(active=True)
context = {
'post': post,
'comments': comments,
'form': form,
}
return render(request, 'posts/detail.html', context)
Update the detail template to include the comments and form:
# Update posts/templates/posts/detail.html — add after the article closing tag
# (but still inside the {% block content %})
<hr style="margin-top: 40px;">
<section>
<h2>Comments ({{ comments.count }})</h2>
{% if comments %}
{% for comment in comments %}
<div style="margin-bottom: 20px; padding: 10px;
background: #f9f9f9; border-radius: 4px;">
<p><strong>{{ comment.name }}</strong>
<small>{{ comment.created_at|date:"M j, Y" }}</small></p>
<p>{{ comment.body|linebreaks }}</p>
</div>
{% endfor %}
{% else %}
<p>No comments yet. Be the first to share your thoughts!</p>
{% endif %}
</section>
<hr>
<section>
<h2>Leave a Comment</h2>
<form method="post" action="{{ request.path }}">
{% csrf_token %}
<div style="margin-bottom: 15px;">
<label for="{{ form.name.id_for_label }}">Name:</label><br>
{{ form.name }}
{% if form.name.errors %}
<small style="color: red;">{{ form.name.errors }}</small>
{% endif %}
</div>
<div style="margin-bottom: 15px;">
<label for="{{ form.email.id_for_label }}">Email:</label><br>
{{ form.email }}
{% if form.email.errors %}
<small style="color: red;">{{ form.email.errors }}</small>
{% endif %}
</div>
<div style="margin-bottom: 15px;">
<label for="{{ form.body.id_for_label }}">Comment:</label><br>
{{ form.body }}
{% if form.body.errors %}
<small style="color: red;">{{ form.body.errors }}</small>
{% endif %}
</div>
<button type="submit">Submit Comment</button>
</form>
</section>
Critical security note: {% csrf_token %} is mandatory in every POST form. Django validates this token to prevent Cross-Site Request Forgery attacks. If you omit it, submissions will fail with a 403 error.
Step 14: Class-Based Generic Views — Even Less Code
Django ships with generic views that handle common patterns. Let's refactor the post list to use a ListView:
# Alternative posts/views.py using generic views
from django.views.generic import ListView, DetailView
from django.shortcuts import get_object_or_404
from .models import Post
class PostListView(ListView):
"""Generic list view for published blog posts."""
queryset = Post.objects.filter(status='published').select_related('author')
template_name = 'posts/list.html'
context_object_name = 'posts' # Variable name in the template
paginate_by = 10 # Show 10 posts per page with automatic pagination
class PostDetailView(DetailView):
"""Generic detail view for a single post."""
queryset = Post.objects.filter(status='published').select_related('author')
template_name = 'posts/detail.html'
context_object_name = 'post'
slug_url_kwarg = 'slug' # The URL capture group name
Generic views reduce boilerplate dramatically. The ListView automatically handles queryset execution, pagination, and context rendering. The DetailView fetches a single object by slug and returns a 404 if not found — all without writing explicit method code.
Step 15: Static Files — CSS, JavaScript, and Images
Static files are CSS, JavaScript, and images that don't change per request. In development, Django serves them automatically from app-level static/ directories.
Create a static directory and a CSS file:
mkdir -p posts/static/posts/css
/* posts/static/posts/css/blog.css */
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.7;
color: #333;
}
nav {
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 2px solid #e2e8f0;
}
nav a {
margin-right: 20px;
text-decoration: none;
color: #0366d6;
font-weight: 500;
}
nav a:hover {
text-decoration: underline;
}
.post-item {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
}
.post-item h2 {
margin-bottom: 5px;
}
.post-item h2 a {
color: #1a1a1a;
text-decoration: none;
}
.post-item h2 a:hover {
color: #0366d6;
}
.post-meta {
color: #666;
font-size: 0.9em;
margin-bottom: 10px;
}
article h1 {
font-size: 2em;
margin-bottom: 10px;
}
footer {
margin-top: 50px;
border-top: 2px solid #e2e8f0;
padding-top: 20px;
color: #888;
font-size: 0.85em;
}
.comment {
margin-bottom: 20px;
padding: 15px;
background: #f8fafc;
border-radius: 6px;
border: 1px solid #e2e8f0;
}
form button {
background: #0366d6;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
}
form button:hover {
background: #0255b3;
}
.errorlist {
color: #d73a49;
list-style: none;
padding: 0;
margin: 5px 0 0 0;
font-size: 0.85em;
}
Update the base template to load static files:
# At the top of posts/templates/posts/base.html, add:
{% load static %}
# Replace the inline <style> block in the <head> with:
<link rel="stylesheet" href="{% static 'posts/css/blog.css' %}">
The {% load static %} tag loads the static file template library, and {% static 'posts/css/blog.css' %} resolves the URL to the actual file path. In production, this URL will point to your CDN or static file server automatically.
Step 16: Testing — Ensuring Your Code Works
Django's test