ZetCode

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.

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.

config/settings.py
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.

pages/views.py
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.

pages/urls.py
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.

config/urls.py
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.

pages/templates/pages/variables.html
<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.

pages/templates/pages/filters.html
<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.

pages/templates/pages/tags.html
{% 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.

pages/templates/pages/escaping.html
<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: &lt;b&gt;bold&lt;/b&gt; &amp; &lt;i&gt;italic&lt;/i&gt;</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.

templates/base.html
<!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 %}&copy; 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.

pages/templates/pages/inherit.html
{% 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.

pages/templates/pages/_menu.html
<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>&copy; 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.

pages/tests.py
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: &lt;b&gt;bold&lt;/b&gt; &amp; &lt;i&gt;italic&lt;/i&gt;')
        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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Django tutorials.