Django is the most popular Python web framework — used by Instagram, Pinterest, Disqus, and thousands of companies worldwide. It follows the "batteries included" philosophy: authentication, admin panel, ORM, forms, and security come built-in. This tutorial takes you from zero to a working web app.
What you'll learn
| Topic | What you'll be able to do |
|---|---|
| Setup | Install Django and create your first project |
| URLs & Views | Map URLs to Python functions |
| Templates | Render HTML with Django's template engine |
| Models & ORM | Define database models, query data |
| Forms | Handle user input safely |
| Admin | Manage data with Django's built-in admin |
| Authentication | Login, logout, registration |
| REST API | Build JSON APIs with Django REST Framework |
| Deployment | Deploy to a live server |
Why Django?
| Feature | Django | Flask | FastAPI |
|---|---|---|---|
| Philosophy | Batteries included | Minimal/micro | Async-first |
| ORM | Built-in | Third-party (SQLAlchemy) | Third-party |
| Admin panel | Built-in | None | None |
| Auth | Built-in | Third-party | Third-party |
| Learning curve | Medium | Low | Low–Medium |
| Best for | Full web apps, rapid dev | APIs, small apps | High-performance APIs |
| Used by | Instagram, Pinterest | Airbnb (legacy), Netflix | Uber, Microsoft |
Django is the right choice when you want to ship a complete web app quickly without stitching together a dozen packages.
Prerequisites
- Basic Python (variables, functions, classes, lists/dicts)
- Command line basics
- No prior web framework experience needed
1. Installation and Setup
Install Django
# Create a virtual environment (always use one)
python -m venv venv
# Activate it
# macOS/Linux:
source venv/bin/activate
# Windows:
venv\Scripts\activate
# Install Django
pip install django
# Verify
python -m django --version
# 5.x.x
Create a project
django-admin startproject mysite
cd mysite
Project structure
mysite/
├── manage.py # Command-line utility for the project
└── mysite/
├── __init__.py
├── settings.py # Project configuration
├── urls.py # Root URL configuration
├── asgi.py # ASGI entry point
└── wsgi.py # WSGI entry point
Run the development server
python manage.py runserver
# Server started at http://127.0.0.1:8000/
Open http://127.0.0.1:8000/ — you'll see Django's welcome page.
2. Django Apps
Django projects are divided into apps — reusable components, each handling one part of the site.
python manage.py startapp blog
App structure
blog/
├── __init__.py
├── admin.py # Register models with admin
├── apps.py # App configuration
├── migrations/ # Database migrations
├── models.py # Database models
├── tests.py # Tests
└── views.py # View functions
Register the app
# 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
]
3. URLs and Views
Your first view
# blog/views.py
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, Django!")
Wire up URLs
# blog/urls.py (create this file)
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]
# mysite/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls')),
]
Visit http://127.0.0.1:8000/blog/ — you'll see "Hello, Django!".
URL patterns
| Pattern | Matches | Captured as |
|---|---|---|
path('', view) |
/ |
nothing |
path('posts/', view) |
/posts/ |
nothing |
path('posts/<int:pk>/', view) |
/posts/42/ |
pk=42 |
path('posts/<slug:slug>/', view) |
/posts/my-post/ |
slug='my-post' |
path('posts/<str:name>/', view) |
/posts/django/ |
name='django' |
re_path(r'^posts/(?P<year>[0-9]{4})/$', view) |
/posts/2025/ |
year='2025' |
4. Templates
Templates are HTML files with Django template tags.
Set up templates
# mysite/settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # add this
...
},
]
Create a template
<!-- templates/blog/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<h1>{{ title }}</h1>
{% if posts %}
<ul>
{% for post in posts %}
<li><a href="/blog/{{ post.id }}/">{{ post.title }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No posts yet.</p>
{% endif %}
</body>
</html>
Render a template from a view
# blog/views.py
from django.shortcuts import render
def index(request):
context = {
'title': 'My Blog',
'posts': [
{'id': 1, 'title': 'First Post'},
{'id': 2, 'title': 'Second Post'},
]
}
return render(request, 'blog/index.html', context)
Template tags reference
| Tag | Purpose | Example |
|---|---|---|
{{ variable }} |
Output a variable | {{ user.name }} |
{% if %} |
Conditional | {% if user.is_authenticated %} |
{% for %} |
Loop | {% for item in list %} |
{% url %} |
Reverse URL | {% url 'blog:index' %} |
{% block %} |
Template inheritance | {% block content %} |
{% extends %} |
Inherit a base template | {% extends 'base.html' %} |
{% include %} |
Include a partial | {% include 'nav.html' %} |
{% static %} |
Static file URL | {% static 'css/style.css' %} |
{% csrf_token %} |
CSRF protection in forms | Inside <form> tags |
{{ value|filter }} |
Apply a filter | {{ date|date:"Y-m-d" }} |
Template inheritance
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My Site{% endblock %}</title>
</head>
<body>
<nav>...</nav>
{% block content %}{% endblock %}
<footer>...</footer>
</body>
</html>
<!-- templates/blog/index.html -->
{% extends 'base.html' %}
{% block title %}Blog{% endblock %}
{% block content %}
<h1>Blog Posts</h1>
...
{% endblock %}
5. Models and Database
Models define your database schema in Python.
Define a model
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
body = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published = models.BooleanField(default=False)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
Common field types
| Field | Use case | Example |
|---|---|---|
CharField |
Short text | CharField(max_length=200) |
TextField |
Long text | TextField() |
IntegerField |
Integer | IntegerField() |
FloatField |
Float | FloatField() |
BooleanField |
True/False | BooleanField(default=False) |
DateField |
Date | DateField() |
DateTimeField |
Date + time | DateTimeField(auto_now_add=True) |
EmailField |
EmailField() |
|
URLField |
URL | URLField() |
SlugField |
URL-friendly string | SlugField(unique=True) |
ImageField |
Image upload | ImageField(upload_to='images/') |
FileField |
File upload | FileField(upload_to='files/') |
JSONField |
JSON data | JSONField(default=dict) |
ForeignKey |
Many-to-one relation | ForeignKey(User, on_delete=models.CASCADE) |
ManyToManyField |
Many-to-many | ManyToManyField(Tag) |
OneToOneField |
One-to-one | OneToOneField(User, on_delete=models.CASCADE) |
Migrations
Every time you change a model, create and run a migration:
python manage.py makemigrations # create migration files
python manage.py migrate # apply to database
Django ORM — querying data
from blog.models import Post
# Create
post = Post.objects.create(
title="My First Post",
slug="my-first-post",
body="Hello world!",
author=user,
published=True
)
# Read all
posts = Post.objects.all()
# Filter
published = Post.objects.filter(published=True)
by_author = Post.objects.filter(author=user)
# Get single object (raises DoesNotExist if not found)
post = Post.objects.get(pk=1)
post = Post.objects.get(slug='my-first-post')
# Or return None if not found
post = Post.objects.filter(slug='my-first-post').first()
# Order
recent = Post.objects.order_by('-created_at')
# Limit
top5 = Post.objects.all()[:5]
# Update
Post.objects.filter(pk=1).update(published=True)
# Delete
Post.objects.filter(pk=1).delete()
# Count
total = Post.objects.count()
published_count = Post.objects.filter(published=True).count()
# Exists
if Post.objects.filter(slug='my-post').exists():
...
# Complex queries (Q objects)
from django.db.models import Q
results = Post.objects.filter(
Q(title__icontains='django') | Q(body__icontains='django')
)
# Related objects
user = User.objects.get(pk=1)
user_posts = user.post_set.all() # reverse FK lookup
ORM field lookups
| Lookup | SQL equivalent | Example |
|---|---|---|
exact |
= value |
filter(title__exact='Django') |
iexact |
ILIKE value |
filter(title__iexact='django') |
contains |
LIKE %value% |
filter(body__contains='python') |
icontains |
ILIKE %value% |
filter(body__icontains='python') |
startswith |
LIKE value% |
filter(slug__startswith='how-to') |
endswith |
LIKE %value |
filter(slug__endswith='-guide') |
gt / lt |
> / < |
filter(views__gt=100) |
gte / lte |
>= / <= |
filter(created_at__gte=date) |
in |
IN (...) |
filter(pk__in=[1, 2, 3]) |
isnull |
IS NULL |
filter(image__isnull=True) |
6. Django Admin
Django's admin gives you a full CRUD interface for free.
Register models
# blog/admin.py
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ('title', 'author', 'published', 'created_at')
list_filter = ('published', 'author')
search_fields = ('title', 'body')
prepopulated_fields = {'slug': ('title',)}
date_hierarchy = 'created_at'
Create superuser
python manage.py createsuperuser
# Enter username, email, password
Visit http://127.0.0.1:8000/admin/ and log in.
7. Forms
Define a form
# blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'slug', 'body', 'published']
widgets = {
'body': forms.Textarea(attrs={'rows': 10}),
}
Handle form in a view
# blog/views.py
from django.shortcuts import render, redirect
from .forms import PostForm
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('blog:index')
else:
form = PostForm()
return render(request, 'blog/create_post.html', {'form': form})
Render form in template
<!-- templates/blog/create_post.html -->
{% extends 'base.html' %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Publish</button>
</form>
{% endblock %}
Form rendering options
| Method | Output |
|---|---|
{{ form.as_p }} |
Fields wrapped in <p> tags |
{{ form.as_ul }} |
Fields as <li> items |
{{ form.as_table }} |
Fields as table rows |
{{ form.field_name }} |
Single field (manual layout) |
{{ form.field_name.label_tag }} |
Label for a field |
{{ form.field_name.errors }} |
Validation errors for a field |
8. Authentication
Django includes a complete authentication system.
Enable auth URLs
# mysite/urls.py
from django.contrib.auth import views as auth_views
urlpatterns = [
...
path('accounts/login/', auth_views.LoginView.as_view(), name='login'),
path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),
]
Protect views
# blog/views.py
from django.contrib.auth.decorators import login_required
@login_required
def create_post(request):
...
Registration view
# blog/views.py
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import login
def register(request):
if request.method == 'POST':
form = UserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
return redirect('blog:index')
else:
form = UserCreationForm()
return render(request, 'registration/register.html', {'form': form})
Check auth in templates
{% if user.is_authenticated %}
<p>Hello, {{ user.username }}!</p>
<a href="{% url 'logout' %}">Logout</a>
{% else %}
<a href="{% url 'login' %}">Login</a>
{% endif %}
9. Static Files
Configure static files
# mysite/settings.py
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
STATIC_ROOT = BASE_DIR / 'staticfiles' # for production collectstatic
Use static files in templates
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<img src="{% static 'images/logo.png' %}" alt="Logo">
<script src="{% static 'js/main.js' %}"></script>
Collect static for production
python manage.py collectstatic
10. Complete Blog App Example
Here's a complete working blog app putting it all together:
Models
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
body = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
tags = models.ManyToManyField(Tag, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
published = models.BooleanField(default=False)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:post_detail', kwargs={'slug': self.slug})
Views
# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from .models import Post
from .forms import PostForm
def post_list(request):
posts = Post.objects.filter(published=True)
return render(request, 'blog/post_list.html', {'posts': posts})
def post_detail(request, slug):
post = get_object_or_404(Post, slug=slug, published=True)
return render(request, 'blog/post_detail.html', {'post': post})
@login_required
def post_create(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
form.save_m2m() # save ManyToMany (tags)
return redirect(post.get_absolute_url())
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form, 'action': 'Create'})
@login_required
def post_edit(request, slug):
post = get_object_or_404(Post, slug=slug, author=request.user)
if request.method == 'POST':
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
return redirect(post.get_absolute_url())
else:
form = PostForm(instance=post)
return render(request, 'blog/post_form.html', {'form': form, 'action': 'Edit'})
URLs
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.post_list, name='post_list'),
path('<slug:slug>/', views.post_detail, name='post_detail'),
path('create/', views.post_create, name='post_create'),
path('<slug:slug>/edit/', views.post_edit, name='post_edit'),
]
11. Django REST Framework (DRF)
Build JSON APIs with Django REST Framework:
pip install djangorestframework
# mysite/settings.py
INSTALLED_APPS = [
...
'rest_framework',
]
Serializer
# blog/serializers.py
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField()
class Meta:
model = Post
fields = ['id', 'title', 'slug', 'body', 'author', 'created_at', 'published']
API Views
# blog/api_views.py
from rest_framework import generics, permissions
from .models import Post
from .serializers import PostSerializer
class PostListAPI(generics.ListCreateAPIView):
queryset = Post.objects.filter(published=True)
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
class PostDetailAPI(generics.RetrieveUpdateDestroyAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
lookup_field = 'slug'
# blog/urls.py — add API routes
from .api_views import PostListAPI, PostDetailAPI
urlpatterns += [
path('api/posts/', PostListAPI.as_view(), name='api_post_list'),
path('api/posts/<slug:slug>/', PostDetailAPI.as_view(), name='api_post_detail'),
]
12. Django Settings Best Practices
# mysite/settings.py
# Never commit the secret key
import os
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'dev-key-change-in-production')
# Disable debug in production
DEBUG = os.environ.get('DEBUG', 'True') == 'True'
# Allowed hosts — required when DEBUG=False
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost').split(',')
# Database — default is SQLite, use PostgreSQL in production
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME', 'mydb'),
'USER': os.environ.get('DB_USER', 'myuser'),
'PASSWORD': os.environ.get('DB_PASSWORD', ''),
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
13. Deployment
Prepare for production
pip install gunicorn whitenoise psycopg2-binary
# Collect static files
python manage.py collectstatic
# Run database migrations
python manage.py migrate
# mysite/settings.py (production additions)
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
DEBUG = False
# Serve static files with WhiteNoise
MIDDLEWARE = [
'whitenoise.middleware.WhiteNoiseMiddleware',
...
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
Run with Gunicorn
gunicorn mysite.wsgi:application --bind 0.0.0.0:8000 --workers 4
Deployment options
| Platform | Free tier | Complexity | Best for |
|---|---|---|---|
| Railway | Yes | Low | Quick deploys |
| Render | Yes | Low | Hobby/production |
| Heroku | No | Low | Legacy |
| DigitalOcean App Platform | No | Medium | Production |
| AWS EC2 + Nginx + Gunicorn | No | High | Full control |
| Fly.io | Yes | Medium | Global edge |
Django vs Flask vs FastAPI
| Django | Flask | FastAPI | |
|---|---|---|---|
| Type | Full-stack framework | Micro-framework | Modern async framework |
| ORM | Built-in | None (add SQLAlchemy) | None (add SQLAlchemy/Tortoise) |
| Admin | Built-in | None | None |
| Auth | Built-in | Flask-Login | None |
| REST API | DRF (third-party) | Flask-RESTX | Built-in |
| Async support | Partial (Django 4.1+) | Partial | Full |
| Performance | Medium | Medium | High |
| Best for | Full web apps | Small APIs/apps | High-performance APIs |
Common Django Commands
# Project and app management
django-admin startproject mysite # create project
python manage.py startapp blog # create app
# Database
python manage.py makemigrations # create migration files
python manage.py migrate # apply migrations
python manage.py showmigrations # list migrations
python manage.py sqlmigrate blog 0001 # show SQL for migration
# Development
python manage.py runserver # start dev server (port 8000)
python manage.py runserver 8080 # different port
python manage.py shell # interactive Python shell with Django loaded
# Users
python manage.py createsuperuser # create admin user
python manage.py changepassword <user> # change user password
# Static files
python manage.py collectstatic # collect for production
# Testing
python manage.py test # run all tests
python manage.py test blog # run tests for app
# Database dump/load
python manage.py dumpdata blog > blog.json # export data
python manage.py loaddata blog.json # import data
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
Committing SECRET_KEY |
Security vulnerability | Use os.environ.get() |
DEBUG=True in production |
Exposes stack traces | Set DEBUG=False |
| Not using virtual env | Package conflicts | Always use venv |
Forgetting {% csrf_token %} |
Forms broken | Add to every <form> |
| Querying in a loop (N+1) | Slow DB queries | Use select_related() / prefetch_related() |
No ALLOWED_HOSTS |
Django rejects requests | Set for production |
| Not running migrations | DB schema mismatch | Always makemigrations && migrate |
Using objects.get() without try/except |
Unhandled DoesNotExist | Use get_object_or_404() or try/except |
What to Learn Next
| Topic | Why |
|---|---|
| Class-based views (CBV) | Less boilerplate for CRUD operations |
| Django REST Framework | Build production-grade REST APIs |
| Celery + Redis | Background tasks, email sending |
| Django Channels | WebSockets, real-time features |
| pytest-django | Better test tooling |
| django-allauth | Social authentication (Google, GitHub) |
| django-storages | S3 / cloud file storage |
| Wagtail / django-cms | CMS on top of Django |
FAQ
Q: Should I use Django or Flask for beginners?
Django is better for beginners who want to build complete web apps — you get more built-in. Flask is better if you're building a small API or want to learn web fundamentals piece by piece.
Q: Does Django work with React or Vue?
Yes. You can use Django as a backend API (with DRF) and React/Vue as the frontend, or use Django's template engine for server-rendered pages. Many production apps use Django API + React frontend.
Q: What database does Django use?
By default, SQLite (great for development). For production, PostgreSQL is the most popular choice. Django also supports MySQL, MariaDB, and Oracle.
Q: How long does it take to learn Django?
With basic Python knowledge: 2–4 weeks to build simple apps, 3–6 months to be productive professionally. Following this tutorial takes 1–2 days.
Q: Is Django still relevant in 2025?
Yes. Django powers Instagram (one of the world's busiest sites), is actively maintained, and the Django 5.x series added async views, querysets, and improved performance. It remains the go-to Python framework for full web apps.
Q: What's the difference between a Django project and a Django app?
A project is the whole website (settings, root URLs, configuration). An app is a reusable component inside the project (blog, auth, shop). A project can contain many apps.