Django templates
last modified November 13, 2026 Python 3.15
In this article we work with the Django template language. We cover
variables, filters, tags, automatic escaping, the include tag, and
template inheritance with extends and block, and we
look at the TEMPLATES setting. The examples use Django 6.1 and
Python 3.15.
Django templates
A template is a text file, usually HTML, that contains placeholders and simple logic. The template engine replaces them with real values and produces the final document. Django's template language is intentionally limited: it has no arbitrary Python code, which keeps the presentation separate from the business logic. The language has four kinds of constructs.
{{ variable }}- prints a value{{ value|filter }}- a filter transforms the value{% tag %}- a tag controls the flow or loads data; some tags have a closing tag, such as{% endif %}{# comment #}- a comment that is not rendered
The example project
The project is created with uv; the steps are explained in
Django introduction. The commands below create a project
directory, install Django, generate the project with the configuration package
config, and start the app pages.
$ mkdir templates $ cd templates $ uv init --bare $ uv add django $ uv run django-admin startproject config . $ uv run python manage.py startapp pages
The trailing dot in startproject config . creates manage.py and the
config package in the current directory instead of one level deeper. A new app is
ignored by Django until it is listed in INSTALLED_APPS, so we add
the app to the list in config/settings.py; the other
entries are generated by startproject.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'pages',
]
Every listing in this article is a file of the application, shown with a solid blue border and the label Project file.
The app pages has five views. Each view renders one
template with a shared context dictionary, built by the
common_context() function.
from dataclasses import dataclass
from datetime import date
from django.shortcuts import render
@dataclass
class Product:
name: str
price: float
def label(self):
return f'{self.name.upper()} (new)'
def common_context():
return {
'person': {'name': 'alice smith', 'langs': ['Python', 'Rust', 'Go']},
'product': Product('lamp', 1234.5),
'today': date(2026, 11, 13),
'text': 'Django templates are rendered by the template engine',
'count': 3,
'bio': '<b>bold</b> & <i>italic</i>',
'empty_list': [],
'menu': [{'name': 'Home', 'url': '/'}, {'name': 'About', 'url': '/inherit/'}],
}
def variables(request):
return render(request, 'pages/variables.html', common_context())
def filters(request):
return render(request, 'pages/filters.html', common_context())
def tags(request):
return render(request, 'pages/tags.html', common_context())
def escaping(request):
return render(request, 'pages/escaping.html', common_context())
def inherit(request):
return render(request, 'pages/inherit.html', common_context())
The context contains a dictionary, a list, a dataclass instance with a method,
a date, a string with HTML characters, and an empty list. The
Product dataclass is an ordinary Python class; the template engine
can use any object. The render() shortcut is described in
Django views. The URLs are
/variables/, /filters/, /tags/,
/escaping/, and /inherit/. The URLconf of the app maps
them to the views.
from django.urls import path
from . import views
app_name = 'pages'
urlpatterns = [
path('variables/', views.variables, name='variables'),
path('filters/', views.filters, name='filters'),
path('tags/', views.tags, name='tags'),
path('escaping/', views.escaping, name='escaping'),
path('inherit/', views.inherit, name='inherit'),
]
The project's urls.py includes this file at the site root.
from django.urls import include, path
urlpatterns = [
path('', include('pages.urls')),
]
The templates are described in the following sections.
The TEMPLATES setting
The TEMPLATES setting configures the template engines. The
default configuration generated by startproject uses the
DjangoTemplates backend. Two options say where the engine looks for
templates.
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
DIRS lists directories with project-wide templates; the generated
list is empty, and we added BASE_DIR / 'templates', where the base
template will live. With APP_DIRS set to True the engine also
searches the templates subdirectory of every installed app. The
engine tries the DIRS first and then the apps in the order of
INSTALLED_APPS, and uses the first file with a matching name. For this reason app
templates are placed in a subdirectory named after the app, for example
pages/templates/pages/tags.html, so the template name
pages/tags.html is unambiguous. The context processors add
variables, such as request and user, to the context
of every template rendered with a request.
Variables
A variable is written between double braces. The engine resolves a dotted name
by trying, in order, a dictionary lookup, an attribute lookup, and a list-index
lookup; if the result is callable, it is called without arguments. This is why
person.name works for a dictionary, product.name for an
attribute, person.langs.0 for a list item, and
product.label for a method. A variable that does not exist is
replaced with an empty string and no error is raised.
<p>Name: {{ person.name }}</p>
<p>First language: {{ person.langs.0 }}</p>
<p>Product: {{ product.name }}</p>
<p>Label: {{ product.label }}</p>
<p>Missing: [{{ person.age }}]</p>
The template uses a dictionary key, a list index, an attribute, and a method
call. The person.age key is missing, so the brackets are empty. The
engine can be told to treat such variables as errors with the
string_if_invalid option, which is useful in development.
$ curl -s http://127.0.0.1:8000/variables/ <p>Name: alice smith</p> <p>First language: Python</p> <p>Product: lamp</p> <p>Label: LAMP (new)</p> <p>Missing: []</p>
Filters
A filter changes the value of a variable before it is printed. It
is applied with a pipe, {{ value|filter }}; some filters take an
argument after a colon, and filters can be chained. The template below uses
several of the 57 built-in filters.
<p>{{ person.name|title }}</p>
<p>{{ person.name|upper }}</p>
<p>{{ person.age|default:"unknown" }}</p>
<p>{{ person.langs|join:", " }}</p>
<p>{{ person.langs|length }} languages</p>
<p>{{ product.price|floatformat:2 }}</p>
<p>{{ today|date:"j F Y" }}</p>
<p>{{ text|truncatewords:4 }}</p>
<p>{{ count }} item{{ count|pluralize }}</p>
<p>{{ person.name|title|center:20 }}|</p>
The title and upper filters change the case.
default supplies a value for a missing or false variable,
join works like str.join(), and length
returns the size of a sequence. floatformat:2 rounds a number to two
decimal places, date formats a date with the format characters of
the filter (j is the day, F the month name,
Y the year), and truncatewords shortens a text after the
given number of words and appends an ellipsis. pluralize adds an
s when the number is not 1, and center pads a string to
the given width. The vertical bar after the last paragraph shows that
the padding is part of the string.
$ curl -s http://127.0.0.1:8000/filters/ <p>Alice Smith</p> <p>ALICE SMITH</p> <p>unknown</p> <p>Python, Rust, Go</p> <p>3 languages</p> <p>1234.50</p> <p>13 November 2026</p> <p>Django templates are rendered …</p> <p>3 items</p> <p> Alice Smith |</p>
Tags
Tags provide logic. The most common ones are if,
for, with, and url. The
if tag evaluates a condition and supports elif,
else, the operators and, or,
not, in, and the comparisons ==,
!=, <, >, <=, and
>=. The for tag loops over a sequence; inside the
loop the forloop variable offers counter,
counter0, first, last, and others. The
optional empty clause runs when the sequence is empty. The
with tag stores a value in a short name for its block; it is used to
avoid repeating an expensive lookup.
{% if person.langs %}
<p>{{ person.name|title }} knows {{ person.langs|length }} languages:</p>
<ul>
{% for lang in person.langs %}
<li>{{ forloop.counter }}. {{ lang }}{% if forloop.last %} (last){% endif %}</li>
{% endfor %}
</ul>
{% else %}
<p>No languages.</p>
{% endif %}
{% for item in empty_list %}
<p>{{ item }}</p>
{% empty %}
<p>The list is empty.</p>
{% endfor %}
{% if count > 5 %}
<p>Many</p>
{% elif count > 1 and count <= 5 %}
<p>A few</p>
{% else %}
<p>One or none</p>
{% endif %}
{% with total=product.price|floatformat:0 %}
<p>Rounded price: {{ total }}</p>
{% endwith %}
{# This comment is not rendered. #}
<p>Done.</p>
The template uses all of these tags and a comment. The language has no
parentheses in conditions; the logical operators have the usual precedence,
and binds tighter than or. The tags leave blank lines in the
output; we removed them from the listing below to keep it short.
$ curl -s http://127.0.0.1:8000/tags/ <p>Alice Smith knows 3 languages:</p> <ul> <li>1. Python</li> <li>2. Rust</li> <li>3. Go (last)</li> </ul> <p>The list is empty.</p> <p>A few</p> <p>Rounded price: 1235</p> <p>Done.</p>
Automatic escaping
The template engine escapes the characters <,
>, &, ', and " in the
values of variables, so they are not interpreted as HTML. This protects a site
against cross-site scripting (XSS), when a value comes from a user. We can
turn it off for a single variable with the safe filter or for a
block with the autoescape tag.
<p>Default: {{ bio }}</p>
<p>Safe: {{ bio|safe }}</p>
{% autoescape off %}
<p>Off: {{ bio }}</p>
{% endautoescape %}
The value of bio contains tags and an ampersand. The first
paragraph escapes them. The other two print them unchanged, so the browser would
render the text as HTML. Use safe only for HTML that we create
ourselves or have already sanitized; never for user input.
$ curl -s http://127.0.0.1:8000/escaping/ <p>Default: <b>bold</b> & <i>italic</i></p> <p>Safe: <b>bold</b> & <i>italic</i></p> <p>Off: <b>bold</b> & <i>italic</i></p>
Template inheritance
Template inheritance is the most powerful part of the language. We write a
base template with the common structure of the site and mark the parts
that child templates may change with {% block %} tags. A child
template starts with {% extends %} and overrides some of the blocks.
The base template of our project is stored in the project-level
templates directory.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}My site{% endblock %}</title>
</head>
<body>
{% include 'pages/_menu.html' with active='Home' %}
<main>
{% block content %}{% endblock %}
</main>
<footer>{% block footer %}© 2026 My site{% endblock %}</footer>
</body>
</html>
The base template defines three blocks, title,
content, and footer, and includes a menu with the
include tag. The title and the footer have default content that is used
when a child does not override the block.
A child template
The child template extends base.html. The
extends tag must be the first tag in the file. The child overrides
the blocks it needs; the block.super variable inserts the content of
the same block from the parent, so the child can add to the parent's content
instead of replacing it.
{% extends 'base.html' %}
{% block title %}About - {{ block.super }}{% endblock %}
{% block content %}
<h1>About</h1>
<p>This page extends base.html.</p>
{% endblock %}
{% block footer %}{{ block.super }} | Contact{% endblock %}
Anything outside of the blocks in a child template is ignored. The title
block uses block.super to append the site name to the page title, and the
footer adds a link text to the parent's footer. The child does not define the menu;
it comes from the base template through include, described in the
next section.
The include tag
The include tag renders another template inside the current one,
with the current context. It is used for reusable fragments such as menus and
cards. The with option passes additional variables, and the
only keyword restricts the context to those variables. The base
template includes the menu with the active value set to
Home.
<nav>
{% for item in menu %}
<a href="{{ item.url }}"{% if item.name == active %} class="active"{% endif %}>{{ item.name }}</a>
{% endfor %}
</nav>
The fragment loops over the menu list from the context and
marks the active item. The underscore in the file name is a convention for
partial templates. Since active is set by the with
option, the Home link gets the class="active"
attribute. The response of the /inherit/ URL combines the three
templates into one document; blank lines left by the tags are removed.
$ curl -s http://127.0.0.1:8000/inherit/ <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>About - My site</title> </head> <body> <nav> <a href="/" class="active">Home</a> <a href="/inherit/">About</a> </nav> <main> <h1>About</h1> <p>This page extends base.html.</p> </main> <footer>© 2026 My site | Contact</footer> </body> </html>
Rendering a template from code
Templates can be rendered outside of views. The
render_to_string() function from
django.template.loader loads a template by name and returns the
result as a string, which is useful for emails (see
Django email). A template can also be built from a
string with the Template class, as in the command below, which
renders an inline template with the title filter.
$ uv run python manage.py shell --no-imports -c "from django.template import Context, Template; print(Template('Hello, {{ name|title }}!').render(Context({'name': 'jane doe'})))"
Hello, Jane Doe!
Testing templates
The test client records the templates that were used to render a response, so
we can check the inheritance with assertTemplateUsed(). The tests
also verify the auto-escaping and that a missing variable renders as an empty
string.
from django.test import TestCase
from django.urls import reverse
class TemplateTests(TestCase):
def test_inheritance_and_include(self):
response = self.client.get(reverse('pages:inherit'))
self.assertTemplateUsed(response, 'base.html')
self.assertTemplateUsed(response, 'pages/inherit.html')
self.assertTemplateUsed(response, 'pages/_menu.html')
self.assertContains(response, '<title>About - My site</title>')
def test_autoescape(self):
response = self.client.get(reverse('pages:escaping'))
self.assertContains(response, 'Default: <b>bold</b> & <i>italic</i>')
self.assertContains(response, 'Safe: <b>bold</b> & <i>italic</i>')
def test_missing_variable_is_empty(self):
response = self.client.get(reverse('pages:variables'))
self.assertContains(response, 'Missing: []')
The first test checks that the base.html template, the child, and
the included menu were all used, and that the page title was assembled from the
two blocks. The second test confirms the escaped and unescaped forms of the same
value.
$ uv run python manage.py test Found 3 test(s). System check identified no issues (0 silenced). Creating test database for alias 'default'... ... ---------------------------------------------------------------------- Ran 3 tests in 0.007s OK Destroying test database for alias 'default'...
Source
Django documentation: Templates
Django documentation: Built-in template tags and filters
Django documentation: The Django template language
In this article we have used variables, filters, and tags of the Django template language, seen automatic escaping, and built a page from a base template, a child template, and an included fragment. The article Django custom template tags and filters shows how to extend the language, and Django static files explains how to load stylesheets and images in templates. See also Django views.
Author
List all Django tutorials.