Quick Takeaways
What you'll learn in this article
- 1
Python 3.12 or higher (Django 6.0 dropped support for Python 3.10 and 3.11)
- 2
A terminal (Terminal on Mac, PowerShell/WSL on Windows, any Linux shell)
- 3
A text editor (VS Code, PyCharm, Cursor, or your preferred editor)
- 4
A project is your entire web application—settings, configuration, URL routing
- 5
An app is a modular component—users, blog, tasks, payments
Keep reading for detailed implementation, code examples, and real-world results
Django has been powering the web's most demanding applications for nearly two decades. Instagram. Pinterest. Mozilla. The Washington Post. NASA. These organizations didn't choose Django because it was trendy—they chose it because it works.
Now, with Django 6.0 released in December 2025, the framework has evolved to meet modern development demands: built-in background tasks, native Content Security Policy support, template partials, and matured async capabilities. Python 3.12+ only—the framework has officially left legacy Python behind.
This tutorial will take you from zero to a working Django application. Not a toy demo, but something real: a task management app with user authentication, database models, forms, and a clean interface. The skills translate directly to production development.
Let's build something.
What You'll Need
Before we start, ensure you have:
- Python 3.12 or higher (Django 6.0 dropped support for Python 3.10 and 3.11)
- A terminal (Terminal on Mac, PowerShell/WSL on Windows, any Linux shell)
- A text editor (VS Code, PyCharm, Cursor, or your preferred editor)
- 30-45 minutes of focused time
Check your Python version:
python3 --version
If you see Python 3.12.x or higher, you're ready. If not, install the latest Python from python.org or use your system's package manager.
Part 1: Environment Setup
Professional Python development requires virtual environments. Always. No exceptions.
Why Virtual Environments Matter
Without virtual environments, you install packages globally. This works until you have two projects: Project A needs Django 5.2, Project B needs Django 6.0. Conflict. Virtual environments solve this by creating isolated Python installations for each project.
Create Your Project Directory
mkdir taskflow cd taskflow
Create and Activate Virtual Environment
python3 -m venv venv
This creates a venv directory containing an isolated Python installation.
Activate it:
macOS/Linux:
source venv/bin/activate
Windows (PowerShell):
.\venv\Scripts\Activate.ps1
Windows (Command Prompt):
venv\Scripts\activate.bat
Your terminal prompt should now show (venv) at the beginning, indicating the virtual environment is active.
Install Django
pip install django
As of February 2026, this installs Django 6.0.1. Verify:
python -m django --version
You should see 6.0.1 or similar.
Create Requirements File
Track your dependencies from the start:
pip freeze > requirements.txt
This creates a requirements.txt file that anyone can use to recreate your exact environment:
pip install -r requirements.txt
Part 2: Create Your Django Project
Django distinguishes between projects and apps:
- A project is your entire web application—settings, configuration, URL routing
- An app is a modular component—users, blog, tasks, payments
One project contains multiple apps. This modularity is Django's superpower.
Start the Project
django-admin startproject config .
The . at the end is crucial—it creates the project in your current directory rather than nesting it. Your structure now looks like:
taskflow/ ├── config/ │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── requirements.txt └── venv/
I named the project config rather than the typical taskflow because this directory holds configuration, not business logic. Clean naming matters.
Run the Development Server
python manage.py runserver
Open your browser to http://127.0.0.1:8000/. You should see Django's welcome page—a rocket ship confirming everything works.
Press Ctrl+C to stop the server.
Important: Django's development server is for local testing only. Never use it in production.
Part 3: Create Your First App
Let's create the tasks app:
python manage.py startapp tasks
New structure:
taskflow/ ├── config/ │ └── ... ├── tasks/ │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations/ │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── manage.py └── venv/
Register the App
Django doesn't automatically detect new apps. You must register them.
Open config/settings.py and find INSTALLED_APPS:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Add your app here
'tasks',
]
Part 4: Define Your Models
Models define your data structure. Django's ORM (Object-Relational Mapper) translates Python classes into database tables.
Open tasks/models.py:
from django.db import models
from django.contrib.auth.models import User
class Task(models.Model):
"""A task in our task management system."""
class Priority(models.TextChoices):
LOW = 'low', 'Low'
MEDIUM = 'medium', 'Medium'
HIGH = 'high', 'High'
class Status(models.TextChoices):
TODO = 'todo', 'To Do'
IN_PROGRESS = 'in_progress', 'In Progress'
DONE = 'done', 'Done'
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
due_date = models.DateField(null=True, blank=True)
priority = models.CharField(
max_length=10,
choices=Priority.choices,
default=Priority.MEDIUM
)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.TODO
)
owner = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='tasks'
)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
Let's break this down:
- TextChoices: Django 6's clean way to define field choices
- CharField: Text with a maximum length
- TextField: Unlimited text (for longer descriptions)
- DateTimeField(auto_now_add=True): Automatically sets timestamp when created
- DateTimeField(auto_now=True): Automatically updates timestamp on every save
- ForeignKey: Creates a relationship to the User model
- on_delete=models.CASCADE: When a user is deleted, delete their tasks too
- related_name='tasks': Access a user's tasks via user.tasks.all()
- Meta ordering: Default sort order for querysets
Create and Apply Migrations
Migrations track database changes:
python manage.py makemigrations
Output:
Migrations for 'tasks':
tasks/migrations/0001_initial.py
- Create model Task
Apply the migration to create the database table:
python manage.py migrate
Django uses SQLite by default—perfect for development. The database file (db.sqlite3) appears in your project root.
Part 5: Django Admin
Django's admin interface is legendary. Full CRUD (Create, Read, Update, Delete) functionality with zero additional code.
Create a Superuser
python manage.py createsuperuser
Enter a username, email, and password when prompted.
Register Your Model with Admin
Open tasks/admin.py:
from django.contrib import admin
from .models import Task
@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
list_display = ['title', 'owner', 'priority', 'status', 'due_date', 'created_at']
list_filter = ['status', 'priority', 'created_at']
search_fields = ['title', 'description']
date_hierarchy = 'created_at'
ordering = ['-created_at']
Start the server and visit http://127.0.0.1:8000/admin/:
python manage.py runserver
Log in with your superuser credentials. You now have a fully functional admin interface for managing tasks.
Create a few test tasks to work with.
Part 6: Views and URLs
Views handle HTTP requests and return responses. Django supports both function-based views (FBVs) and class-based views (CBVs). We'll use both.
Create Views
Open tasks/views.py:
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Task
from .forms import TaskForm
class TaskListView(LoginRequiredMixin, ListView):
"""Display all tasks for the logged-in user."""
model = Task
template_name = 'tasks/task_list.html'
context_object_name = 'tasks'
def get_queryset(self):
return Task.objects.filter(owner=self.request.user)
class TaskDetailView(LoginRequiredMixin, DetailView):
"""Display a single task."""
model = Task
template_name = 'tasks/task_detail.html'
context_object_name = 'task'
def get_queryset(self):
return Task.objects.filter(owner=self.request.user)
class TaskCreateView(LoginRequiredMixin, CreateView):
"""Create a new task."""
model = Task
form_class = TaskForm
template_name = 'tasks/task_form.html'
success_url = reverse_lazy('tasks:task_list')
def form_valid(self, form):
form.instance.owner = self.request.user
return super().form_valid(form)
class TaskUpdateView(LoginRequiredMixin, UpdateView):
"""Update an existing task."""
model = Task
form_class = TaskForm
template_name = 'tasks/task_form.html'
success_url = reverse_lazy('tasks:task_list')
def get_queryset(self):
return Task.objects.filter(owner=self.request.user)
class TaskDeleteView(LoginRequiredMixin, DeleteView):
"""Delete a task."""
model = Task
template_name = 'tasks/task_confirm_delete.html'
success_url = reverse_lazy('tasks:task_list')
def get_queryset(self):
return Task.objects.filter(owner=self.request.user)
@login_required
def task_toggle_status(request, pk):
"""Quick toggle between todo/in_progress/done."""
task = get_object_or_404(Task, pk=pk, owner=request.user)
status_cycle = {
Task.Status.TODO: Task.Status.IN_PROGRESS,
Task.Status.IN_PROGRESS: Task.Status.DONE,
Task.Status.DONE: Task.Status.TODO,
}
task.status = status_cycle[task.status]
task.save()
return redirect('tasks:task_list')
Create Forms
Create tasks/forms.py:
from django import forms
from .models import Task
class TaskForm(forms.ModelForm):
"""Form for creating and updating tasks."""
class Meta:
model = Task
fields = ['title', 'description', 'due_date', 'priority', 'status']
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter task title'
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Add a description (optional)'
}),
'due_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
'priority': forms.Select(attrs={
'class': 'form-control'
}),
'status': forms.Select(attrs={
'class': 'form-control'
}),
}
Create App URLs
Create tasks/urls.py:
from django.urls import path
from . import views
app_name = 'tasks'
urlpatterns = [
path('', views.TaskListView.as_view(), name='task_list'),
path('task/<int:pk>/', views.TaskDetailView.as_view(), name='task_detail'),
path('task/create/', views.TaskCreateView.as_view(), name='task_create'),
path('task/<int:pk>/update/', views.TaskUpdateView.as_view(), name='task_update'),
path('task/<int:pk>/delete/', views.TaskDeleteView.as_view(), name='task_delete'),
path('task/<int:pk>/toggle/', views.task_toggle_status, name='task_toggle'),
]
Connect to Project URLs
Update config/urls.py:
from django.contrib import admin
from django.urls import path, include
from django.views.generic import RedirectView
urlpatterns = [
path('admin/', admin.site.urls),
path('tasks/', include('tasks.urls')),
path('accounts/', include('django.contrib.auth.urls')),
path('', RedirectView.as_view(url='/tasks/', permanent=False)),
]
Part 7: Templates
Django's template system separates logic from presentation.
Create Template Directory Structure
mkdir -p tasks/templates/tasks mkdir -p templates/registration
Base Template
Create tasks/templates/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 %}TaskFlow{% endblock %}</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<style>
body {
background-color: #f8f9fa;
}
.task-card {
transition: transform 0.2s;
}
.task-card:hover {
transform: translateY(-2px);
}
.priority-high {
border-left: 4px solid #dc3545;
}
.priority-medium {
border-left: 4px solid #ffc107;
}
.priority-low {
border-left: 4px solid #28a745;
}
.status-badge {
font-size: 0.75rem;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container">
<a class="navbar-brand" href="{% url 'tasks:task_list' %}">TaskFlow</a>
<div class="navbar-nav ms-auto">
{% if user.is_authenticated %}
<span class="navbar-text me-3">Hello, {{ user.username }}</span>
<a class="nav-link" href="{% url 'logout' %}">Logout</a>
{% else %}
<a class="nav-link" href="{% url 'login' %}">Login</a>
{% endif %}
</div>
</div>
</nav>
<main class="container">
{% if messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show">
{{ message }}
<button
type="button"
class="btn-close"
data-bs-dismiss="alert"
></button>
</div>
{% endfor %} {% endif %} {% block content %}{% endblock %}
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
Task List Template
Create tasks/templates/tasks/task_list.html:
{% extends 'base.html' %} {% block title %}My Tasks | TaskFlow{% endblock %} {%
block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>My Tasks</h1>
<a href="{% url 'tasks:task_create' %}" class="btn btn-primary">
+ New Task
</a>
</div>
{% if tasks %}
<div class="row">
{% for task in tasks %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card task-card priority-{{ task.priority }}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title mb-0">
<a
href="{% url 'tasks:task_detail' task.pk %}"
class="text-decoration-none text-dark"
>
{{ task.title }}
</a>
</h5>
<span
class="badge status-badge
{% if task.status == 'done' %}bg-success
{% elif task.status == 'in_progress' %}bg-primary
{% else %}bg-secondary{% endif %}"
>
{{ task.get_status_display }}
</span>
</div>
{% if task.description %}
<p class="card-text text-muted small">
{{ task.description|truncatewords:20 }}
</p>
{% endif %}
<div class="d-flex justify-content-between align-items-center mt-3">
<small class="text-muted">
{% if task.due_date %} Due: {{ task.due_date }} {% else %} No due
date {% endif %}
</small>
<div class="btn-group btn-group-sm">
<a
href="{% url 'tasks:task_toggle' task.pk %}"
class="btn btn-outline-secondary"
title="Toggle Status"
>
↻
</a>
<a
href="{% url 'tasks:task_update' task.pk %}"
class="btn btn-outline-primary"
title="Edit"
>
✎
</a>
<a
href="{% url 'tasks:task_delete' task.pk %}"
class="btn btn-outline-danger"
title="Delete"
>
×
</a>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<h3 class="text-muted">No tasks yet</h3>
<p class="text-muted">Create your first task to get started.</p>
<a href="{% url 'tasks:task_create' %}" class="btn btn-primary btn-lg">
Create Task
</a>
</div>
{% endif %} {% endblock %}
Task Detail Template
Create tasks/templates/tasks/task_detail.html:
{% extends 'base.html' %} {% block title %}{{ task.title }} | TaskFlow{%
endblock %} {% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<h1 class="card-title">{{ task.title }}</h1>
<span
class="badge
{% if task.status == 'done' %}bg-success
{% elif task.status == 'in_progress' %}bg-primary
{% else %}bg-secondary{% endif %} fs-6"
>
{{ task.get_status_display }}
</span>
</div>
<div class="row mb-4">
<div class="col-sm-4">
<strong>Priority:</strong>
<span class="text-capitalize">{{ task.get_priority_display }}</span>
</div>
<div class="col-sm-4">
<strong>Due Date:</strong>
{{ task.due_date|default:"Not set" }}
</div>
<div class="col-sm-4">
<strong>Created:</strong>
{{ task.created_at|date:"M d, Y" }}
</div>
</div>
{% if task.description %}
<div class="mb-4">
<h5>Description</h5>
<p class="text-muted">{{ task.description|linebreaks }}</p>
</div>
{% endif %}
<div class="d-flex gap-2">
<a
href="{% url 'tasks:task_toggle' task.pk %}"
class="btn btn-secondary"
>
Toggle Status
</a>
<a
href="{% url 'tasks:task_update' task.pk %}"
class="btn btn-primary"
>
Edit Task
</a>
<a
href="{% url 'tasks:task_delete' task.pk %}"
class="btn btn-danger"
>
Delete Task
</a>
<a
href="{% url 'tasks:task_list' %}"
class="btn btn-outline-secondary ms-auto"
>
← Back to List
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Task Form Template
Create tasks/templates/tasks/task_form.html:
{% extends 'base.html' %} {% block title %}{% if form.instance.pk %}Edit{% else
%}Create{% endif %} Task | TaskFlow{% endblock %} {% block content %}
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card">
<div class="card-body">
<h1 class="card-title mb-4">
{% if form.instance.pk %}Edit Task{% else %}Create New Task{% endif %}
</h1>
<form method="post">
{% csrf_token %} {% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">
{{ field.label }}
</label>
{{ field }} {% if field.errors %}
<div class="invalid-feedback d-block">
{{ field.errors|join:", " }}
</div>
{% endif %} {% if field.help_text %}
<small class="form-text text-muted"> {{ field.help_text }} </small>
{% endif %}
</div>
{% endfor %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
{% if form.instance.pk %}Update{% else %}Create{% endif %} Task
</button>
<a
href="{% url 'tasks:task_list' %}"
class="btn btn-outline-secondary"
>
Cancel
</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
Delete Confirmation Template
Create tasks/templates/tasks/task_confirm_delete.html:
{% extends 'base.html' %} {% block title %}Delete Task | TaskFlow{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card border-danger">
<div class="card-body text-center">
<h1 class="card-title text-danger mb-4">Delete Task?</h1>
<p class="lead">Are you sure you want to delete:</p>
<p class="fs-4 fw-bold">{{ task.title }}</p>
<p class="text-muted">This action cannot be undone.</p>
<form method="post" class="mt-4">
{% csrf_token %}
<button type="submit" class="btn btn-danger btn-lg me-2">
Yes, Delete
</button>
<a
href="{% url 'tasks:task_list' %}"
class="btn btn-secondary btn-lg"
>
Cancel
</a>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
Login Template
Create templates/registration/login.html:
{% extends 'base.html' %} {% block title %}Login | TaskFlow{% endblock %} {%
block content %}
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-body">
<h1 class="card-title text-center mb-4">Login</h1>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="id_username" class="form-label">Username</label>
<input
type="text"
name="username"
id="id_username"
class="form-control"
required
autofocus
/>
</div>
<div class="mb-3">
<label for="id_password" class="form-label">Password</label>
<input
type="password"
name="password"
id="id_password"
class="form-control"
required
/>
</div>
{% if form.errors %}
<div class="alert alert-danger">Invalid username or password.</div>
{% endif %}
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
Update Settings for Templates
In config/settings.py, update the TEMPLATES setting:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # Add this line
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
Also add at the bottom of settings.py:
LOGIN_REDIRECT_URL = 'tasks:task_list' LOGOUT_REDIRECT_URL = 'login'
Part 8: Test Your Application
Run the server:
python manage.py runserver
Visit http://127.0.0.1:8000/. You should be redirected to the login page.
Log in with your superuser credentials. You can now:
- View your task list
- Create new tasks
- Edit existing tasks
- Toggle task status
- Delete tasks
- Log out
Django 6.0: What's New
While this tutorial covered Django fundamentals that work across versions, Django 6.0 brings significant enhancements worth knowing:
Built-in Tasks Framework
Django 6.0 includes a native background tasks framework. No more relying solely on Celery for simple async work:
from django.tasks import task
@task
def send_notification_email(user_id, message):
# This runs in the background
user = User.objects.get(pk=user_id)
send_mail(
subject='Task Update',
message=message,
recipient_list=[user.email],
)
# Queue the task
send_notification_email.enqueue(user_id=1, message='Your task is complete!')
The framework handles task creation and queuing—execution requires external infrastructure like Django-Q2 or a custom worker.
Content Security Policy Middleware
Native CSP support protects against XSS attacks:
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.middleware.csp.ContentSecurityPolicyMiddleware', # New in 6.0
# ...
]
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "cdn.jsdelivr.net")
CSP_STYLE_SRC = ("'self'", "cdn.jsdelivr.net", "'unsafe-inline'")
Template Partials
Reuse template fragments without separate files:
{% partialdef task_card %}
<div class="card">
<h5>{{ task.title }}</h5>
<p>{{ task.description }}</p>
</div>
{% endpartialdef %}
<!-- Use it later in the same template -->
{% for task in tasks %} {% partial task_card %} {% endfor %}
Async Improvements
AsyncPaginator and AsyncPage enable fully async views:
from django.core.paginator import AsyncPaginator
async def task_list_async(request):
tasks = Task.objects.filter(owner=request.user)
paginator = AsyncPaginator(tasks, 25)
page = await paginator.aget_page(request.GET.get('page'))
return render(request, 'tasks/task_list.html', {'page': page})
Next Steps
You've built a functional Django application. Here's where to go next:
Extend the App:
- Add user registration (django-allauth or build your own)
- Implement task categories or tags
- Add due date notifications
- Create an API with Django REST Framework
Learn More Django:
- Official Django Tutorial - Go deeper on each concept
- Django REST Framework - Build APIs
- Django Debug Toolbar - Debug performance
- Django 6.0 Release Notes - Full feature list
Production Deployment:
- Use PostgreSQL instead of SQLite
- Configure environment variables for secrets
- Set up Gunicorn or uWSGI
- Deploy to Railway, Render, or AWS
Testing:
- Write unit tests with pytest-django
- Test views with Django's test client
- Add integration tests for user flows
Conclusion
Django remains the most productive full-stack Python framework for a reason. The "batteries included" philosophy—admin interface, ORM, authentication, forms, templating—means you spend time building features instead of assembling infrastructure.
Django 6.0 continues this tradition while embracing modern patterns: async support, background tasks, and security features that would have required third-party packages just two years ago.
The skills you've learned here scale directly to production applications. Instagram didn't start with a different framework and migrate—they started with Django and scaled it to billions of users.
Now build something.
The complete source code for this tutorial is available at github.com/CrashBytes/ByteSizedExamples/tree/main/taskflow-django-tutorial.

