Django templates are text files that define the structure and layout of your web pages. They combine static HTML with dynamic content using Django's template language, providing a clean separation between presentation and business logic.
Templates are files containing static content mixed with special syntax for dynamic content:
<!-- blog/templates/blog/post_detail.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{ post.title }} - My Blog</title>
</head>
<body>
<h1>{{ post.title }}</h1>
<p>By {{ post.author.username }} on {{ post.created_at|date:"F d, Y" }}</p>
<div class="content">
{{ post.content|linebreaks }}
</div>
</body>
</html>
Variables - Display dynamic content
{{ variable_name }}
{{ object.attribute }}
{{ dictionary.key }}
Tags - Control logic and flow
{% if condition %}
<p>Content when true</p>
{% endif %}
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
Filters - Transform variable output
{{ text|lower }}
{{ date|date:"Y-m-d" }}
{{ content|truncatewords:50 }}
Comments - Documentation that won't appear in output
{# This is a comment #}
{% comment %}
Multi-line comment
for documentation
{% endcomment %}
# settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
BASE_DIR / 'templates', # Project-level templates
],
'APP_DIRS': True, # Look for templates in app directories
'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',
],
},
},
]
myproject/
├── templates/ # Project-level templates
│ ├── base.html # Base template
│ ├── 404.html # Error pages
│ ├── 500.html
│ └── includes/ # Reusable components
│ ├── header.html
│ ├── footer.html
│ └── navigation.html
├── blog/
│ └── templates/
│ └── blog/ # App-specific templates
│ ├── post_list.html
│ ├── post_detail.html
│ └── post_form.html
└── accounts/
└── templates/
└── registration/ # Authentication templates
├── login.html
└── signup.html
<!-- templates/blog/post_list.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Posts</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.post { border-bottom: 1px solid #eee; padding: 20px 0; }
.post h2 { margin: 0 0 10px 0; }
.meta { color: #666; font-size: 0.9em; }
</style>
</head>
<body>
<h1>Latest Blog Posts</h1>
{% for post in posts %}
<article class="post">
<h2>{{ post.title }}</h2>
<p class="meta">
By {{ post.author.get_full_name|default:post.author.username }}
on {{ post.created_at|date:"F d, Y" }}
</p>
<p>{{ post.excerpt|default:post.content|truncatewords:30 }}</p>
<a href="{% url 'blog:post_detail' post.pk %}">Read more</a>
</article>
{% empty %}
<p>No posts available yet.</p>
{% endfor %}
</body>
</html>
Function-Based View:
# blog/views.py
from django.shortcuts import render
from .models import Post
def post_list(request):
posts = Post.objects.filter(published=True).order_by('-created_at')
context = {
'posts': posts,
'page_title': 'Latest Posts',
}
return render(request, 'blog/post_list.html', context)
Class-Based View:
# blog/views.py
from django.views.generic import ListView
from .models import Post
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
return Post.objects.filter(published=True).order_by('-created_at')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = 'Latest Posts'
return context
Simple Variables:
# views.py
context = {
'title': 'My Blog',
'user_count': 150,
'is_featured': True,
'tags': ['django', 'python', 'web'],
}
<!-- template.html -->
<h1>{{ title }}</h1>
<p>We have {{ user_count }} users!</p>
{% if is_featured %}
<span class="featured">Featured Content</span>
{% endif %}
<ul>
{% for tag in tags %}
<li>{{ tag }}</li>
{% endfor %}
</ul>
Object Attributes:
# models.py
class Post(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def get_absolute_url(self):
return reverse('blog:post_detail', kwargs={'pk': self.pk})
@property
def word_count(self):
return len(self.content.split())
<!-- template.html -->
<h2>{{ post.title }}</h2>
<p>By {{ post.author.username }}</p>
<p>Created: {{ post.created_at }}</p>
<p>Word count: {{ post.word_count }}</p>
<a href="{{ post.get_absolute_url }}">Read more</a>
Dictionary Access:
# views.py
context = {
'user_stats': {
'total': 1000,
'active': 750,
'new_today': 25,
},
'settings': {
'site_name': 'My Blog',
'maintenance_mode': False,
}
}
<!-- template.html -->
<h1>{{ settings.site_name }}</h1>
<p>Total users: {{ user_stats.total }}</p>
<p>Active users: {{ user_stats.active }}</p>
{% if not settings.maintenance_mode %}
<p>Site is operational</p>
{% endif %}
Django searches for templates in this order:
TEMPLATES['DIRS']templates/ folders in installed apps# When you call render(request, 'blog/post_list.html', context)
# Django looks for:
# 1. /path/to/project/templates/blog/post_list.html
# 2. /path/to/blog/templates/blog/post_list.html
# 3. /path/to/other_app/templates/blog/post_list.html
Good Practice - Namespace Templates:
blog/templates/blog/post_list.html # ✓ Namespaced
accounts/templates/accounts/login.html # ✓ Namespaced
Bad Practice - No Namespace:
blog/templates/post_list.html # ✗ Could conflict
accounts/templates/login.html # ✗ Could conflict
Context processors add variables to every template context:
# settings.py
TEMPLATES = [{
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug', # {{ debug }}
'django.template.context_processors.request', # {{ request }}
'django.contrib.auth.context_processors.auth', # {{ user }}, {{ perms }}
'django.contrib.messages.context_processors.messages', # {{ messages }}
],
},
}]
Using Built-in Context Variables:
<!-- Available in all templates -->
<p>Hello, {{ user.username }}!</p>
<p>Current path: {{ request.path }}</p>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% if debug %}
<div class="debug-info">Debug mode is on</div>
{% endif %}
# blog/context_processors.py
from .models import Category
def blog_context(request):
"""Add blog-specific context to all templates."""
return {
'site_name': 'My Awesome Blog',
'categories': Category.objects.all(),
'recent_posts': Post.objects.filter(published=True)[:5],
}
# settings.py
TEMPLATES = [{
'OPTIONS': {
'context_processors': [
# ... built-in processors
'blog.context_processors.blog_context',
],
},
}]
<!-- Now available in all templates -->
<h1>{{ site_name }}</h1>
<nav>
{% for category in categories %}
<a href="{% url 'blog:category' category.slug %}">{{ category.name }}</a>
{% endfor %}
</nav>
<aside>
<h3>Recent Posts</h3>
{% for post in recent_posts %}
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
{% endfor %}
</aside>
Django automatically escapes variables to prevent XSS attacks:
# views.py
context = {
'user_input': '<script>alert("XSS")</script>',
'safe_html': '<strong>Bold text</strong>',
}
<!-- template.html -->
{{ user_input }} <!-- Outputs: <script>alert("XSS")</script> -->
{{ safe_html }} <!-- Outputs: <strong>Bold text</strong> -->
<!-- To output unescaped HTML (use carefully!) -->
{{ safe_html|safe }} <!-- Outputs: <strong>Bold text</strong> -->
# views.py
from django.utils.safestring import mark_safe
def my_view(request):
# Unsafe - will be escaped
unsafe_content = '<p>This will be escaped</p>'
# Safe - marked as safe HTML
safe_content = mark_safe('<p>This is safe HTML</p>')
context = {
'unsafe_content': unsafe_content,
'safe_content': safe_content,
}
return render(request, 'template.html', context)
<!-- template.html -->
{{ unsafe_content }} <!-- Escaped -->
{{ safe_content }} <!-- Not escaped -->
<!-- Alternative: use the safe filter -->
{{ unsafe_content|safe }} <!-- Not escaped - be careful! -->
TemplateDoesNotExist:
# Common causes:
# 1. Wrong template path
return render(request, 'blog/nonexistent.html', context)
# 2. Missing app in INSTALLED_APPS
# 3. Incorrect TEMPLATES configuration
TemplateSyntaxError:
<!-- Common syntax errors -->
{% if condition % <!-- Missing closing % -->
{{ variable.missing_closing_brace
{% for item in items %} <!-- Missing {% endfor %} -->
VariableDoesNotExist:
<!-- Accessing undefined variables -->
{{ undefined_variable }} <!-- Returns empty string in production -->
{{ object.nonexistent_attribute }} <!-- Returns empty string -->
Debug Mode (DEBUG=True):
Production Mode (DEBUG=False):
blog/
└── templates/
└── blog/
├── base.html
├── post_list.html
├── post_detail.html
├── post_form.html
└── includes/
├── post_card.html
└── pagination.html
templates/ # Project-level templates
├── base.html # Site-wide base
├── includes/
│ ├── header.html
│ ├── footer.html
│ └── navigation.html
└── errors/
├── 404.html
└── 500.html
blog/templates/blog/ # Blog-specific templates
├── post_list.html
└── post_detail.html
accounts/templates/registration/ # Auth templates
├── login.html
└── signup.html
Consistent Naming:
# List views
post_list.html
category_list.html
user_list.html
# Detail views
post_detail.html
category_detail.html
user_detail.html
# Form views
post_form.html
post_create.html
post_update.html
# Partial templates
_post_card.html
_pagination.html
_form_errors.html
# settings.py - Enable template caching in production
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'OPTIONS': {
'loaders': [
('django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
]),
],
'context_processors': [
# ... context processors
],
},
}]
# Efficient - only pass what's needed
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
context = {
'post': post,
'related_posts': post.get_related_posts()[:3], # Limit results
}
return render(request, 'blog/post_detail.html', context)
# Inefficient - passing unnecessary data
def post_detail_bad(request, pk):
context = {
'all_posts': Post.objects.all(), # Too much data
'all_users': User.objects.all(), # Unnecessary
'post': get_object_or_404(Post, pk=pk),
}
return render(request, 'blog/post_detail.html', context)
Django templates provide a powerful, secure, and flexible way to generate dynamic content. Understanding these fundamentals prepares you for more advanced template features and optimization techniques.
Templates and Presentation Layer
Django's template system is a powerful, flexible framework for generating dynamic HTML, XML, and other text-based formats. This comprehensive section covers everything you need to master Django templates, from basic syntax to advanced optimization techniques.
The Django Template Language
The Django Template Language (DTL) is a powerful, secure, and designer-friendly templating system. It provides a clean syntax for displaying dynamic content while maintaining separation between presentation and business logic.