Django is the Python web framework that has powered everything from Instagram to Pinterest to Mozilla. It ships with an admin panel, an ORM, authentication, and URL routing all built in, so you spend less time stitching libraries together and more time building features. With Django 6.0 now current (released December 2025), the framework has added native background tasks, template partials, and built-in Content Security Policy support. This guide walks through the fundamentals of building web applications with Django and highlights the features that make version 6.0 worth adopting.
Whether you are building a blog, a SaaS product, or an internal business tool, Django provides the scaffolding to get from an empty directory to a working application faster than assembling a framework from individual packages. Its "batteries included" philosophy means that common web development tasks like database migrations, user authentication, and form validation are handled by the framework itself rather than by third-party add-ons. That design choice has kept Django relevant for over two decades.
Why Django and How It Works
Django follows the Model-View-Template (MVT) architectural pattern, which is its own take on the classic Model-View-Controller (MVC) approach. In MVT, the Model defines your data structure and handles all interaction with the database. The View contains the business logic that processes requests and returns responses. The Template handles presentation, rendering HTML with dynamic data passed from the view. Django itself plays the role of the controller, routing incoming requests to the correct view based on URL patterns.
This separation of concerns keeps your code organized as projects scale. A developer working on database queries does not need to touch the HTML templates, and a front-end contributor can modify templates without worrying about breaking the ORM layer.
Django 6.0 requires Python 3.12, 3.13, or 3.14. If you are running Python 3.10 or 3.11, you will need to upgrade before installing the latest version. The Django 5.2 LTS series is the last to support Python 3.10 and 3.11.
Beyond MVT, Django includes an automatic admin interface that generates a full CRUD panel for your models, a powerful ORM that lets you write database queries in Python instead of raw SQL, a migration system that tracks and applies schema changes, and a built-in authentication system with user management, permissions, and session handling. These are not optional plugins. They ship with every Django installation.
Setting Up Your First Django Project
Getting started with Django takes just a few terminal commands. First, create an isolated Python environment and install the framework.
# Create a virtual environment
python -m venv .venv
# Activate it (macOS/Linux)
source .venv/bin/activate
# Activate it (Windows)
.venv\Scripts\activate
# Install Django
pip install django==6.0.3
# Verify the installation
python -m django --version
With Django installed, you can scaffold a new project and create your first application within it. Django draws a distinction between a project (the overall site configuration) and an app (a modular component that handles a specific piece of functionality).
# Create a new project called "mysite"
django-admin startproject mysite
# Move into the project directory
cd mysite
# Create an app called "blog"
python manage.py startapp blog
# Run the development server
python manage.py runserver
After running startproject, your directory structure will include manage.py (a command-line utility for managing the project), and a package directory with settings.py, urls.py, wsgi.py, and asgi.py. The startapp command creates a separate directory for your blog app with its own models.py, views.py, admin.py, and tests.py files.
After creating a new app, remember to add it to the INSTALLED_APPS list in settings.py. Django will not recognize the app's models, templates, or static files until it is registered there.
Models, Views, and Templates
Defining Models
Models are Python classes that map to database tables. Each attribute on a model class corresponds to a column in that table. Django's ORM translates these class definitions into SQL behind the scenes, so you rarely need to write raw queries.
# blog/models.py
from django.db import models
from django.utils import timezone
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
author = models.ForeignKey(
"auth.User",
on_delete=models.CASCADE,
related_name="posts",
)
published_date = models.DateTimeField(default=timezone.now)
is_published = models.BooleanField(default=False)
class Meta:
ordering = ["-published_date"]
def __str__(self):
return self.title
Once your model is defined, Django's migration system converts it into database schema changes. Running python manage.py makemigrations generates a migration file, and python manage.py migrate applies it to the database. This workflow makes it straightforward to evolve your schema over time without writing manual ALTER TABLE statements.
Writing Views
Views handle the logic that determines what data to retrieve and which template to render. Django supports both function-based views and class-based views. Function-based views are more explicit and easier to follow for beginners. Class-based views provide reusable patterns for common operations like listing objects or displaying detail pages.
# blog/views.py
from django.shortcuts import render, get_object_or_404
from .models import Post
def post_list(request):
posts = Post.objects.filter(is_published=True)
return render(request, "blog/post_list.html", {"posts": posts})
def post_detail(request, slug):
post = get_object_or_404(Post, slug=slug, is_published=True)
return render(request, "blog/post_detail.html", {"post": post})
The render function combines a template with a context dictionary and returns an HttpResponse. The get_object_or_404 shortcut retrieves a single object from the database or raises an HTTP 404 error if it does not exist, which prevents the need for manual try/except blocks around every query.
Building Templates
Templates are HTML files with Django's template language embedded in them. You can use variables, filters, loops, and conditionals to render dynamic content.
<!-- blog/templates/blog/post_list.html -->
{% extends "base.html" %}
{% block content %}
<h1>Latest Posts</h1>
{% for post in posts %}
<article>
<h2>
<a href="{% url 'post_detail' post.slug %}">
{{ post.title }}
</a>
</h2>
<p>{{ post.content|truncatewords:50 }}</p>
<time>{{ post.published_date|date:"F j, Y" }}</time>
</article>
{% empty %}
<p>No posts published yet.</p>
{% endfor %}
{% endblock %}
The {% extends "base.html" %} tag tells Django that this template inherits from a base layout. Blocks defined in the base template (like {% block content %}) can be overridden in child templates. This inheritance system eliminates the need to duplicate header, navigation, and footer markup across every page.
URL Routing and Forms
URL Configuration
Django uses a centralized URL configuration to map URL patterns to views. Each app typically has its own urls.py file, which is then included in the project-level URL configuration.
# blog/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("", views.post_list, name="post_list"),
path("<slug:slug>/", views.post_detail, name="post_detail"),
]
# 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")),
]
The path function maps a URL pattern to a view. The angle bracket syntax (<slug:slug>) captures a portion of the URL and passes it as a keyword argument to the view function. Named URLs (the name parameter) let you reference URLs in templates using the {% url %} tag instead of hardcoding paths, which makes your application resilient to URL changes.
Handling Forms
Django's form system handles rendering, validation, and error messaging. You define form classes that map to your model fields, and Django takes care of generating the HTML inputs and validating submitted data.
# blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "slug", "content", "is_published"]
widgets = {
"content": forms.Textarea(attrs={"rows": 12}),
}
# blog/views.py (adding a create view)
from django.shortcuts import redirect
from .forms import PostForm
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()
return redirect("post_detail", slug=post.slug)
else:
form = PostForm()
return render(request, "blog/post_form.html", {"form": form})
The ModelForm class automatically generates form fields from your model definition. When the form is submitted, calling form.is_valid() runs all field-level and model-level validation. If validation fails, the form object carries error messages that you can display in the template. Using commit=False on form.save() lets you modify the model instance (like setting the author) before writing it to the database.
What Is New in Django 6.0
Django 6.0, released in December 2025, is the current feature release and introduces several capabilities that address long-standing community requests. The latest patch version is 6.0.3, released on March 3, 2026.
Template Partials
Template partials allow you to define reusable named fragments within a single template file using the new {% partialdef %} and {% partial %} tags. Previously, reusing a section of a template required splitting it into a separate file and using {% include %}. Partials eliminate that overhead and pair especially well with HTMX for building interactive server-rendered pages.
<!-- Define a partial and render it inline -->
{% partialdef search_results inline %}
<ul>
{% for item in results %}
<li>{{ item.title }}</li>
{% endfor %}
</ul>
{% endpartialdef %}
<!-- Reuse the same partial elsewhere in the template -->
<div id="sidebar">
{% partial search_results %}
</div>
A view can also render a single partial in isolation using the template_name#partial_name syntax, which makes it straightforward to return HTML fragments in response to HTMX requests without creating separate template files for each fragment.
Background Tasks Framework
Django 6.0 ships with a built-in tasks framework under django.tasks. This provides a standard API for defining and enqueuing background work without requiring third-party task queues for the task definition layer. Django handles task creation, validation, and queuing. Execution is handled by external worker processes.
from django.core.mail import send_mail
from django.tasks import task
@task
def send_welcome_email(user_email, username):
send_mail(
subject="Welcome aboard",
message=f"Hi {username}, thanks for signing up.",
from_email=None,
recipient_list=[user_email],
)
# Enqueue the task from a view or signal
send_welcome_email.enqueue(
user_email="new_user@example.com",
username="pythonista",
)
The built-in tasks framework does not replace Celery or similar tools for complex workflows requiring retries, scheduling, or distributed workers. It provides a unified interface for task definition that backend implementations (including third-party ones) can plug into. Django ships with ImmediateBackend (synchronous execution) and DummyBackend (no execution) out of the box.
Content Security Policy Support
Django 6.0 adds native support for Content Security Policy headers through ContentSecurityPolicyMiddleware. CSP helps protect applications against cross-site scripting and other content injection attacks by telling the browser which sources of content are allowed to load. Policies are configured as Python dictionaries in your settings, using Django-provided constants for directive values.
Modernized Email API
The email system has been updated to use Python's modern email.message.EmailMessage class, replacing the older email.mime approach. This provides cleaner Unicode handling and a more straightforward interface for composing messages.
Deploying Your Django Application
Running python manage.py runserver is fine for development, but production deployments require an application server, a reverse proxy, and proper static file handling. The typical production stack uses Gunicorn as the WSGI application server (or Uvicorn/Daphne for ASGI applications), Nginx as the reverse proxy, and a managed database like PostgreSQL.
# Install Gunicorn
pip install gunicorn
# Run the application with Gunicorn
gunicorn mysite.wsgi:application --bind 0.0.0.0:8000 --workers 3
# Collect static files for production
python manage.py collectstatic
Before deploying, there are several important settings to configure. Set DEBUG = False in production, define your ALLOWED_HOSTS list, configure a persistent database (Django defaults to SQLite, which is not recommended for production workloads), and set up proper static file serving through a CDN or web server. Running python manage.py check --deploy will audit your settings and flag common security misconfigurations.
Use environment variables for sensitive settings like SECRET_KEY and database credentials. The python-decouple or django-environ packages make this straightforward. Never commit secrets to version control.
For teams that want to avoid managing infrastructure directly, platforms like Railway, Render, and Fly.io offer Django-friendly deployment options with minimal configuration. These services typically handle the application server, database provisioning, and SSL certificates, letting you focus on writing code rather than managing servers.
Key Takeaways
- Django's MVT pattern keeps projects organized: Models handle data, views handle logic, and templates handle presentation. This separation scales well from solo projects to large team codebases.
- The batteries-included philosophy saves time: Authentication, admin panels, form validation, ORM, and migrations all ship with the framework. You do not need to evaluate, install, and integrate separate libraries for these core concerns.
- Django 6.0 addresses real developer pain points: Template partials simplify HTMX-driven interactivity, the background tasks framework provides a standard API for async work, and built-in CSP support makes security headers easier to implement.
- The migration system protects your database: Schema changes are tracked as versioned migration files, making it safe to evolve your data model over time without manual SQL intervention.
- Deployment requires a production-ready stack: Replace the development server with Gunicorn or Uvicorn, put Nginx in front, switch to PostgreSQL, and configure your security settings before going live.
Django has remained one of the leading Python web frameworks for over twenty years because it delivers a complete, well-documented, and secure development experience out of the box. Whether you are building your first web application or migrating an existing project to Django 6.0, the framework provides the structure and tooling to move efficiently from concept to production. Start with the official Django tutorial at docs.djangoproject.com, build something small, and expand from there.