Toolmingo
Guides14 min read

Django Tutorial for Beginners (2025): Build Web Apps with Python

Complete Django tutorial for absolute beginners. Learn Django from scratch: models, views, templates, URLs, forms, authentication, REST APIs, and deploy your first web app. Free guide with code examples.

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 Email 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.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools