Quick Answer:

To build web apps with Django, install it with pip install django, create a project with django-admin startproject mysite, define URL patterns in urls.py, write views in views.py, create templates in HTML, and define data models in models.py. Run python manage.py runserver to start your development server.

Introduction to Django Web Development

Django is the most popular Python web framework, powering sites like Instagram, Pinterest, and Mozilla. It follows the “batteries included” philosophy, providing everything you need to build production-ready web applications out of the box — from an ORM and authentication system to an admin panel and form handling.

If you have been writing Python scripts and want to move into web development, Django gives you a structured, well-documented path. This tutorial walks you through building a complete web application from scratch, covering project setup, URL routing, views, templates, models, and forms. By the end, you will have a working task manager application and a solid understanding of how Django ties everything together.

Setting Up Your Django Project

Start by creating a virtual environment and installing Django. This keeps your project dependencies isolated from your system Python.

# Create and activate a virtual environment
python -m venv myenv
# On macOS/Linux:
source myenv/bin/activate
# On Windows:
myenv\Scripts\activate

# Install Django
pip install django

# Verify the installation
python -m django --version
# Output: 5.1.3

Now create a new Django project and an app within it. A project is the overall configuration container, while apps are modular components that handle specific functionality.

# Create the project
django-admin startproject taskmanager

# Navigate into the project
cd taskmanager

# Create an app called 'tasks'
python manage.py startapp tasks

Your project structure now looks like this:

taskmanager/
    manage.py
    taskmanager/
        __init__.py
        settings.py      # Project configuration
        urls.py           # Root URL routing
        asgi.py
        wsgi.py
    tasks/
        __init__.py
        admin.py          # Admin panel configuration
        apps.py
        models.py         # Database models
        tests.py
        views.py          # Request handlers
        migrations/       # Database migrations

Register your new app in settings.py by adding it to INSTALLED_APPS:

# taskmanager/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'tasks',  # Add your app here
]

Understanding URL Routing

Django uses URL patterns to map incoming HTTP requests to the correct view function. Think of it as a switchboard that directs each request to the right handler.

# taskmanager/urls.py (root URL configuration)
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('tasks.urls')),  # Delegate to app URLs
]

Create a urls.py file inside your tasks app:

# tasks/urls.py
from django.urls import path
from . import views

app_name = 'tasks'

urlpatterns = [
    path('', views.task_list, name='task_list'),
    path('create/', views.task_create, name='task_create'),
    path('<int:pk>/update/', views.task_update, name='task_update'),
    path('<int:pk>/delete/', views.task_delete, name='task_delete'),
    path('<int:pk>/', views.task_detail, name='task_detail'),
]

The <int:pk> syntax captures an integer from the URL and passes it to the view as the pk parameter. Django provides several path converters: str, int, slug, uuid, and path.

Writing Views

Views are Python functions (or classes) that receive HTTP requests and return HTTP responses. They contain the logic for what happens when a user visits a particular URL.

# tasks/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from .models import Task
from .forms import TaskForm


def task_list(request):
    """Display all tasks, with optional filtering."""
    status_filter = request.GET.get('status', '')
    
    if status_filter:
        tasks = Task.objects.filter(status=status_filter)
    else:
        tasks = Task.objects.all()
    
    tasks = tasks.order_by('-created_at')
    
    context = {
        'tasks': tasks,
        'current_filter': status_filter,
        'status_choices': Task.STATUS_CHOICES,
    }
    return render(request, 'tasks/task_list.html', context)


def task_detail(request, pk):
    """Display a single task's details."""
    task = get_object_or_404(Task, pk=pk)
    return render(request, 'tasks/task_detail.html', {'task': task})


def task_create(request):
    """Handle task creation with a form."""
    if request.method == 'POST':
        form = TaskForm(request.POST)
        if form.is_valid():
            task = form.save()
            messages.success(request, f'Task "{task.title}" created successfully!')
            return redirect('tasks:task_list')
    else:
        form = TaskForm()
    
    return render(request, 'tasks/task_form.html', {
        'form': form,
        'action': 'Create',
    })


def task_update(request, pk):
    """Handle task editing."""
    task = get_object_or_404(Task, pk=pk)
    
    if request.method == 'POST':
        form = TaskForm(request.POST, instance=task)
        if form.is_valid():
            form.save()
            messages.success(request, f'Task "{task.title}" updated!')
            return redirect('tasks:task_detail', pk=task.pk)
    else:
        form = TaskForm(instance=task)
    
    return render(request, 'tasks/task_form.html', {
        'form': form,
        'action': 'Update',
        'task': task,
    })


def task_delete(request, pk):
    """Handle task deletion with confirmation."""
    task = get_object_or_404(Task, pk=pk)
    
    if request.method == 'POST':
        title = task.title
        task.delete()
        messages.success(request, f'Task "{title}" deleted.')
        return redirect('tasks:task_list')
    
    return render(request, 'tasks/task_confirm_delete.html', {'task': task})

The get_object_or_404 shortcut is a clean way to handle missing records — it automatically returns a 404 page if the object does not exist, so you do not need manual try/except blocks.

Defining Models

Models define your database structure. Each model class maps to a database table, and each attribute maps to a column. Django’s ORM handles all the SQL behind the scenes.

# tasks/models.py
from django.db import models
from django.utils import timezone


class Task(models.Model):
    STATUS_CHOICES = [
        ('todo', 'To Do'),
        ('in_progress', 'In Progress'),
        ('done', 'Done'),
    ]
    
    PRIORITY_CHOICES = [
        ('low', 'Low'),
        ('medium', 'Medium'),
        ('high', 'High'),
    ]
    
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    status = models.CharField(
        max_length=20,
        choices=STATUS_CHOICES,
        default='todo'
    )
    priority = models.CharField(
        max_length=20,
        choices=PRIORITY_CHOICES,
        default='medium'
    )
    due_date = models.DateField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        ordering = ['-created_at']
    
    def __str__(self):
        return self.title
    
    @property
    def is_overdue(self):
        """Check if task is past its due date."""
        if self.due_date and self.status != 'done':
            return self.due_date < timezone.now().date()
        return False

After defining your model, create and apply the migration to update your database:

# Create migration files
python manage.py makemigrations tasks

# Apply migrations to the database
python manage.py migrate

Django generates the SQL for you. You can inspect it with python manage.py sqlmigrate tasks 0001 if you are curious about what is happening under the hood.

Working with the Django ORM

The ORM provides an intuitive Python API for database operations. Here are the most common patterns you will use daily:

# Creating records
task = Task.objects.create(
    title='Learn Django',
    description='Complete the tutorial',
    priority='high'
)

# Querying records
all_tasks = Task.objects.all()
high_priority = Task.objects.filter(priority='high')
first_task = Task.objects.first()
specific = Task.objects.get(pk=1)  # Raises DoesNotExist if not found

# Chaining filters
urgent = Task.objects.filter(
    priority='high',
    status='todo'
).order_by('due_date')

# Updating records
task.status = 'in_progress'
task.save()

# Bulk update
Task.objects.filter(status='todo').update(status='in_progress')

# Deleting records
task.delete()

# Aggregation
from django.db.models import Count
status_counts = Task.objects.values('status').annotate(
    count=Count('id')
)

Creating Templates

Templates are HTML files with Django's template language mixed in. They handle the presentation layer of your application. Create a templates directory structure inside your tasks app:

tasks/
    templates/
        tasks/
            base.html
            task_list.html
            task_detail.html
            task_form.html
            task_confirm_delete.html

Start with a base template that other templates extend:

<!-- tasks/templates/tasks/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 %}Task Manager{% endblock %}</title>
    <style>
        body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        .nav { background: #333; padding: 10px 20px; border-radius: 8px; margin-bottom: 20px; }
        .nav a { color: white; text-decoration: none; margin-right: 15px; }
        .task-card { border: 1px solid #ddd; padding: 15px; margin: 10px 0; border-radius: 8px; }
        .priority-high { border-left: 4px solid #e74c3c; }
        .priority-medium { border-left: 4px solid #f39c12; }
        .priority-low { border-left: 4px solid #27ae60; }
        .btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; }
        .btn-primary { background: #3498db; color: white; }
        .btn-danger { background: #e74c3c; color: white; }
        .message { padding: 10px 15px; margin: 10px 0; border-radius: 4px; background: #d4edda; color: #155724; }
    </style>
</head>
<body>
    <nav class="nav">
        <a href="{% url 'tasks:task_list' %}">All Tasks</a>
        <a href="{% url 'tasks:task_create' %}">New Task</a>
    </nav>

    {% if messages %}
        {% for message in messages %}
            <div class="message">{{ message }}</div>
        {% endfor %}
    {% endif %}

    {% block content %}{% endblock %}
</body>
</html>

Then create the task list template:

<!-- tasks/templates/tasks/task_list.html -->
{% extends 'tasks/base.html' %}

{% block title %}My Tasks{% endblock %}

{% block content %}
<h1>My Tasks</h1>

<div>
    <strong>Filter:</strong>
    <a href="{% url 'tasks:task_list' %}">All</a>
    {% for value, label in status_choices %}
        | <a href="?status={{ value }}">{{ label }}</a>
    {% endfor %}
</div>

{% for task in tasks %}
    <div class="task-card priority-{{ task.priority }}">
        <h3><a href="{% url 'tasks:task_detail' task.pk %}">{{ task.title }}</a></h3>
        <p>Status: {{ task.get_status_display }} | Priority: {{ task.get_priority_display }}</p>
        {% if task.is_overdue %}
            <p style="color: red;">Overdue! Due: {{ task.due_date }}</p>
        {% elif task.due_date %}
            <p>Due: {{ task.due_date }}</p>
        {% endif %}
    </div>
{% empty %}
    <p>No tasks found. <a href="{% url 'tasks:task_create' %}">Create one!</a></p>
{% endfor %}
{% endblock %}

Building Forms

Django forms handle validation, rendering, and security (CSRF protection) automatically. Model forms are especially useful because they generate form fields directly from your model definition.

# tasks/forms.py
from django import forms
from .models import Task


class TaskForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ['title', 'description', 'status', 'priority', 'due_date']
        widgets = {
            'title': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Enter task title'
            }),
            'description': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 4,
                'placeholder': 'Describe the task...'
            }),
            'due_date': forms.DateInput(attrs={
                'type': 'date',
                'class': 'form-control'
            }),
        }
    
    def clean_title(self):
        """Custom validation for the title field."""
        title = self.cleaned_data['title']
        if len(title) < 3:
            raise forms.ValidationError('Title must be at least 3 characters.')
        return title

And the template that renders the form:

<!-- tasks/templates/tasks/task_form.html -->
{% extends 'tasks/base.html' %}

{% block title %}{{ action }} Task{% endblock %}

{% block content %}
<h1>{{ action }} Task</h1>

<form method="post">
    {% csrf_token %}
    
    {% for field in form %}
        <div style="margin: 10px 0;">
            <label for="{{ field.id_for_label }}">{{ field.label }}</label><br>
            {{ field }}
            {% if field.errors %}
                <p style="color: red;">{{ field.errors.0 }}</p>
            {% endif %}
        </div>
    {% endfor %}
    
    <button type="submit" class="btn btn-primary">{{ action }} Task</button>
</form>
{% endblock %}

Setting Up the Admin Panel

One of Django's most loved features is its automatic admin interface. With just a few lines, you get a fully functional admin panel for managing your data.

# tasks/admin.py
from django.contrib import admin
from .models import Task


@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
    list_display = ['title', 'status', 'priority', 'due_date', 'created_at']
    list_filter = ['status', 'priority']
    search_fields = ['title', 'description']
    list_editable = ['status', 'priority']
    date_hierarchy = 'created_at'

Create a superuser account to access the admin panel:

# Create admin account
python manage.py createsuperuser

# Start the server
python manage.py runserver

# Visit http://127.0.0.1:8000/admin/

Adding Static Files and Media

Most web apps need CSS, JavaScript, and user-uploaded files. Django has a built-in system for managing both static files (your code's assets) and media files (user uploads).

# taskmanager/settings.py

# Static files (CSS, JavaScript, images you ship with the app)
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']

# Media files (user uploads)
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Using static files in templates
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<img src="{% static 'images/logo.png' %}" alt="Logo">

Deploying Your Django App

When you are ready to go live, Django needs a few configuration changes. Here is a deployment checklist covering the essentials:

# taskmanager/settings.py - Production settings

import os

# SECURITY: Never expose your secret key in production
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')

# Disable debug mode
DEBUG = False

# Specify allowed hostnames
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']

# Use a production database (PostgreSQL recommended)
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_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True

For production, use Gunicorn as your WSGI server and Nginx as a reverse proxy:

# Install gunicorn
pip install gunicorn

# Collect static files for production
python manage.py collectstatic

# Run with gunicorn
gunicorn taskmanager.wsgi:application --bind 0.0.0.0:8000

Real-Life Example: Building a Team Task Board

Let us extend our task manager into a team collaboration tool. This shows how Django handles user relationships, permissions, and more complex queries in a practical scenario.

# tasks/models.py - Extended for team use
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone


class Team(models.Model):
    name = models.CharField(max_length=100)
    members = models.ManyToManyField(User, related_name='teams')
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.name


class Task(models.Model):
    STATUS_CHOICES = [
        ('todo', 'To Do'),
        ('in_progress', 'In Progress'),
        ('review', 'In Review'),
        ('done', 'Done'),
    ]
    
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='todo')
    assigned_to = models.ForeignKey(
        User, on_delete=models.SET_NULL,
        null=True, blank=True, related_name='assigned_tasks'
    )
    team = models.ForeignKey(
        Team, on_delete=models.CASCADE, related_name='tasks'
    )
    due_date = models.DateField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.title


class Comment(models.Model):
    task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name='comments')
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    text = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ['created_at']


# Team dashboard view
def team_dashboard(request, team_id):
    team = get_object_or_404(Team, pk=team_id)
    
    # Get task statistics
    stats = {
        'total': team.tasks.count(),
        'todo': team.tasks.filter(status='todo').count(),
        'in_progress': team.tasks.filter(status='in_progress').count(),
        'done': team.tasks.filter(status='done').count(),
        'overdue': team.tasks.filter(
            due_date__lt=timezone.now().date()
        ).exclude(status='done').count(),
    }
    
    # Get tasks grouped by status for a kanban-style board
    columns = {}
    for value, label in Task.STATUS_CHOICES:
        columns[label] = team.tasks.filter(status=value).select_related('assigned_to')
    
    return render(request, 'tasks/team_dashboard.html', {
        'team': team,
        'stats': stats,
        'columns': columns,
    })

Troubleshooting Common Django Issues

IssueCauseSolution
TemplateDoesNotExistTemplate path or app not in INSTALLED_APPSCheck template directory structure and verify app is registered in settings.py
NoReverseMatchURL name doesn't match or missing argumentsVerify URL names in urls.py and pass all required arguments in template tags
OperationalError: no such tableMigrations not appliedRun python manage.py makemigrations then python manage.py migrate
CSRF verification failedMissing {% csrf_token %} in formAdd {% csrf_token %} inside every <form method="post"> tag
Static files not loadingMissing {% load static %} or wrong STATIC_URLAdd {% load static %} at top of template and check settings.py paths
Changes not reflectingBrowser cache or server not restartedHard refresh browser (Ctrl+Shift+R) and restart runserver

Frequently Asked Questions

What is the difference between a Django project and a Django app?

A project is the entire web application with its settings and configuration. An app is a modular component within the project that handles a specific feature. A project can contain many apps, and apps can be reused across projects. For example, a "tasks" app, a "users" app, and a "notifications" app might all live within one project.

Should I use function-based views or class-based views in Django?

Start with function-based views because they are more explicit and easier to understand. Class-based views are useful when you have repetitive patterns like CRUD operations, since Django provides generic views (ListView, CreateView, UpdateView, DeleteView) that reduce boilerplate. Most real projects use a mix of both depending on the complexity of each view.

How do I handle user authentication in Django?

Django includes a complete authentication system out of the box. Add django.contrib.auth to INSTALLED_APPS (it is there by default), use the @login_required decorator on views, and include django.contrib.auth.urls in your URL configuration for login, logout, and password reset views. For registration, create a custom view using Django's UserCreationForm.

Which database should I use with Django?

SQLite works fine for development and small projects — Django uses it by default. For production, PostgreSQL is the recommended choice because it has the best Django support, including full-text search, JSONField, and ArrayField. MySQL and MariaDB are also supported. The Django ORM makes switching databases straightforward since your Python code stays the same.

How do I deploy a Django app to production?

Set DEBUG = False, configure a production database (PostgreSQL), use environment variables for secrets, run collectstatic, and serve with Gunicorn behind Nginx. Popular hosting options include DigitalOcean, Railway, Render, and AWS. For managed platforms, services like Heroku and PythonAnywhere simplify the process significantly. Always run python manage.py check --deploy before going live.