Templates

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Django 6.1 (6.1.1, released 2026-09-02; 6.1.2 is scheduled for 2026-10-06), Python 3.12-3.14 and PostgreSQL 15 or newer, all checked on 2026-10-03; where the 5.2 LTS (5.2.17) behaves differently the text says so. Every example was run on that stack in a scratch project (the catalog app of the Bookshelf scenario) with manage.py check, the Django test runner (43 tests), the runserver development server and curl, on SQLite, with Jinja2 3.1.6 installed for the Jinja2 backend; none of the examples needs PostgreSQL or Redis. A template is the part of Django that turns data into text: HTML for browsers, but equally plain text for emails, CSV or configuration files. This page explains where templates sit in the request cycle, how the TEMPLATES setting configures one or several engines, the Django template language (DTL) from variables to inheritance and auto-escaping, a reference of every built-in tag and filter, context processors, the lower-level Engine/Template/Context API, template loaders and caching, how to override a template that belongs to another app, the Jinja2 backend, how to write a backend of your own, how to keep templates "stupid", and how all of this behaves with async views.

In a hurry: what goes where

Need Rule of thumb Section

Render a page from a view

render(request, "catalog/book_list.html", context); the request makes context processors and CSRF available.

Where templates sit in the request cycle

Configure where templates live

TEMPLATES: a project templates/ directory in DIRS, plus APP_DIRS = True for <app>/templates/.

Engines and the TEMPLATES setting

Share a layout between pages

{% extends %} and {% block %}; reuse fragments with {% include %}.

Template inheritance, include and the base template

Output user data safely

Do nothing: auto-escaping is on. Reach for safe, autoescape off and mark_safe as rarely as possible.

Auto-escaping and safe output

Look up the right tag or filter

The reference tables list all of them; {% querystring %} and forloop.length are the newest.

The built-in tags and filters

Add data to every template

A context processor listed in OPTIONS["context_processors"].

Context processors

Use templates outside a view or project

get_template, render_to_string, engines["django"].from_string, or a standalone Engine.

The template API for Python programmers

Change how or where templates are found

OPTIONS["loaders"]: filesystem, app directories, cached, locmem, or your own loader.

Template loaders and caching

Replace a template of Django or a third-party app

Same path in a directory that is searched earlier; {% extends %} the original to change only a block.

Overriding templates

Use Jinja2 or another template language

Add a second entry to TEMPLATES; write a BaseEngine subclass for a language Django does not ship.

The Jinja2 backend, Writing a custom template backend

Keep logic out of templates

Prepare data in the view or model, keep queries out of loops, prefer filters over tags.

"Stupid templates": what stays out of a template

Render under ASGI

Rendering is synchronous; evaluate querysets in the view before rendering.

Templates and async

Where templates sit in the request cycle

By the time a template is rendered the framework has already done most of its work: the handler built the request, the middleware chain ran inwards, the URL resolver picked a view and the view ran its queries. Rendering is usually the last step of the view itself. The render() shortcut loads a template through the configured engines, renders it with the context you pass (and, because it receives the request, with the output of the context processors and a CSRF token) and wraps the resulting string in an HttpResponse. The response then travels back out through the middleware, which can add headers or replace the response.

sequenceDiagram participant B as Browser participant M as Middleware chain participant V as View participant L as Loaders participant E as Engine and Template B->>M: GET /books/ M->>V: request (after URL resolution) V->>V: queries, business logic V->>L: render(request, name, context) L-->>E: cached compiled Template (or load, compile) E->>E: RequestContext, processors, tags, filters E-->>V: HTML string V-->>M: HttpResponse M-->>B: response with headers added

Two variations matter in practice. A view that returns a TemplateResponse (or SimpleTemplateResponse) postpones the rendering: the object travels out through the middleware first, middleware may adjust its template or context through the process_template_response hook, and the handler renders it just before sending. That is how decorators and middleware can still influence a "rendered" page; the details are on Responses, Streaming, CSV & PDF and Middleware. The second variation is that templates are not only for views: error views (404.html, 500.html, covered on Function-Based Views & Shortcuts), emails, admin screens and form widgets all render through the same engines. Where the request cycle as a whole is explained is The Request/Response Cycle.

Engines and the TEMPLATES setting

A project can use one template engine, several, or none. The TEMPLATES setting is a list with one dictionary per engine; its default value is empty, and startproject writes the configuration that nearly every project keeps. Each dictionary accepts five top-level keys.

Key Meaning

BACKEND

Dotted path of a class that implements the template backend API. Built in: django.template.backends.django.DjangoTemplates and django.template.backends.jinja2.Jinja2.

NAME

The engine’s alias, used with using= and as the key of django.template.engines. Defaults to the last component of the backend’s module path ("django", "jinja2"); it must be set explicitly, and be unique, when two engines use the same backend.

DIRS

Directories searched for template sources, in order. Use forward slashes even on Windows.

APP_DIRS

Whether to search a conventional subdirectory of every installed app: templates for the DTL engine, jinja2 for the Jinja2 engine, and whatever app_dirname a custom backend declares.

OPTIONS

Backend-specific settings; see the next table.

The DjangoTemplates backend (a thin wrapper around django.template.Engine) takes these OPTIONS.

Option Default Effect

autoescape

True

HTML auto-escaping of every variable. Set it to False only for an engine that renders non-HTML text.

context_processors

[]

Dotted paths of callables that receive the request and return a dictionary merged into the context of requests rendered with a request.

debug

the value of DEBUG

Stores extra debug information so the technical 500 page can show the offending template line.

loaders

depends on DIRS and APP_DIRS

List of loader classes (strings, or tuples of class and arguments). When omitted, Django builds filesystem and, with APP_DIRS, app-directories loaders and wraps them in the cached loader.

string_if_invalid

""

What a misspelled or missing variable renders as. A debugging aid only.

file_charset

"utf-8"

Charset used to read template files from disk.

libraries

{}

Mapping of label to dotted path of a tag library module; the label is what {% load %} uses.

builtins

[]

Dotted paths of tag library modules whose tags and filters are available without {% load %}.

The settings of the Bookshelf scratch project use all five keys and most options, with a DTL engine first, a Jinja2 engine second and a custom engine third (the last two are the subject of The Jinja2 backend and Writing a custom template backend).

config/settings.py (TEMPLATES excerpt)
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "NAME": "django",
        "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",
                "catalog.context_processors.site_info",
            ],
            "builtins": ["catalog.templatetags.shortcuts"],
        },
    },
    {
        "BACKEND": "django.template.backends.jinja2.Jinja2",
        "NAME": "jinja2",
        "DIRS": [BASE_DIR / "jinja2"],
        "APP_DIRS": True,
        "OPTIONS": {"environment": "config.jinja2.environment"},
    },
    {
        "BACKEND": "catalog.backends.PercentEngine",
        "NAME": "percent",
        "DIRS": [BASE_DIR / "percent"],
        "APP_DIRS": True,
        "OPTIONS": {"prefix": "[catalog] "},
    },
]

SECRET_KEY and DEBUG come from the environment in that project (os.environ, with get_random_secret_key() as a development fallback), never from the source file; see Settings & Configuration. Three details of the configuration deserve attention.

  • Search order. get_template() tries every engine in TEMPLATES order and, inside an engine, every directory in DIRS order, then every app. The first match wins and the search stops, which is why the DTL engine, listed first, answers for catalog/book_list.html even though the Jinja2 engine also has a file of that name.

  • Namespacing. Templates of all apps share one namespace. Put them in a subdirectory named after the app (catalog/templates/catalog/book_list.html, referred to as "catalog/book_list.html"), otherwise two apps that both ship an index.html shadow each other.

  • Mixing APP_DIRS and loaders. APP_DIRS only affects the default value of loaders. Setting both raises ImproperlyConfigured: app_dirs must not be set when loaders is defined. when the engine is first used; when you list loaders explicitly, list the app-directories loader in them instead.

Since Django 5.1 template engines implement check(), which plugs into the system-check framework (see System Checks). With a duplicate library label it reports, for example, templates.W003 'static' is used for multiple template tag modules, a mistake that is otherwise silent because the last registration wins.

Loading and rendering: the engine-agnostic API

Django defines one API for loading and rendering templates regardless of the engine behind them. The functions live in django.template.loader.

Call What it does

get_template(template_name, using=None)

Finds and returns a template; tries every engine in order unless using names one. Raises TemplateDoesNotExist when nothing is found and TemplateSyntaxError when a template is found but invalid.

select_template(template_name_list, using=None)

Like get_template() but takes a list of names and returns the first that exists; the idiom for per-object overrides with a fallback.

render_to_string(template_name, context=None, request=None, using=None)

Loads and renders in one call. template_name may be a list (it then uses select_template()).

django.template.engines

Mapping of engine alias to configured engine; engines["django"].from_string("Hello {{ name }}!") compiles a template from a string.

template.render(context=None, request=None)

The method of the returned object. context must be a plain dict (never a Context object, which is a DTL-specific class); with a request the engine makes request and the CSRF token available.

The returned template wraps the engine’s own class, and the parameter list of render() is the same for every backend, which is what lets the Jinja2 and custom engines below plug in. Since Django 6.0 a name of the form template.html#partial_name loads one template partial from a DTL template (see Partials in one paragraph). The next example checks the behaviours that matter: which file wins, how several names are tried, how engines are addressed and what failure looks like.

Loading and selecting (excerpt of catalog/tests.py)
def test_get_select_render(self):
    t = get_template("catalog/footer.html")
    self.assertIn("templates/catalog/footer.html", t.origin.name)
    self.assertEqual(
        select_template(["catalog/missing.html", "catalog/footer.html"]).origin.template_name, "catalog/footer.html"
    )
    with self.assertRaises(TemplateDoesNotExist):
        get_template("nope.html")


def test_engines_by_name(self):
    self.assertEqual(set(engines), {"django", "jinja2", "percent"})
    self.assertEqual(engines["django"].from_string("Hello {{ name }}!").render({"name": "W"}), "Hello W!")


def test_select_by_engine_order(self):
    # same name exists in both engines: django engine is first and wins unless `using` is given
    self.assertEqual(get_template("catalog/book_list.html").backend.name, "django")
    self.assertEqual(get_template("catalog/book_list.html", using="jinja2").backend.name, "jinja2")

The Django template language

The DTL is Django’s own template system. It is deliberately not "Python in HTML": the engine never evaluates arbitrary Python expressions, only the syntax, tags and filters it knows (plus your own extensions). The aim is that a front-end developer can change presentation without being able to damage the logic, and that the same language works for text formats other than HTML. Django’s contrib apps, the admin included, use it, and the documentation recommends it unless you have a pressing reason to choose another backend, especially for reusable apps that ship templates.

A template is a text file. Four constructs are interpreted; everything else is copied to the output unchanged.

Construct Syntax Notes

Variable

{{ book.title }}

Looked up in the context, filtered, escaped and output.

Tag

{% if user.is_authenticated %}

Control flow, output, loading of libraries. Many are block tags with an end tag.

Filter

{{ price|floatformat:2 }}

Transforms a value; chainable; takes at most one argument.

Comment

{# note #} and {% comment %}

{# #} is single-line only; {% comment "optional note" %} spans lines and cannot be nested.

Variables and the dot lookup

A variable name may contain letters, digits and underscores, but may not start with an underscore or be a number. A dot means lookup, and Django tries, in this order, a dictionary lookup (foo["bar"]), an attribute lookup (foo.bar) and a list-index lookup (foo[0]); the first that works wins. If the result is callable it is called with no arguments and the result replaces it. The right-hand side of a dot is always a literal name: in {{ foo.bar }} the string "bar" is looked up, never the value of a variable called bar.

That order has one famous trap: objects that answer dictionary lookups for any key. A defaultdict answers d["items"] with an empty list before the template system ever tries the items() method, so a loop over d.items silently renders nothing; convert to a plain dict first. The same applies to a real key named items, keys or values.

The lookup trap and the other rules of variable resolution (excerpts of catalog/tests.py)
def test_lookup_order_defaultdict_gotcha(self):
    from collections import defaultdict

    d = defaultdict(list)
    d["x"] = 1
    plain = dict(d)
    # d["items"] is tried first and the default factory answers with []
    self.assertEqual(self.render("[{% for k, v in d.items %}{{ k }}{% endfor %}]", d=d), "[]")
    self.assertEqual(self.render("[{% for k, v in d.items %}{{ k }}{% endfor %}]", d=plain), "[x]")


def test_silent_alters_do_not_call(self):
    class Silent(Exception):
        silent_variable_failure = True

    class Thing:
        def boom(self):
            raise Silent()

        def delete(self):
            raise AssertionError("called!")

        delete.alters_data = True

        def needs_arg(self, x):
            return x

        class Klass:
            do_not_call_in_templates = True
            label = "kept"

        klass = Klass

    t = Template("[{{ t.boom }}][{{ t.delete }}][{{ t.needs_arg }}][{{ t.klass.label }}]")
    self.assertEqual(t.render(Context({"t": Thing()})), "[][][][kept]")
    with self.assertRaisesMessage(TemplateSyntaxError, "may not begin with underscores"):
        Template("{{ t._private }}")


def test_string_if_invalid(self):
    engine = Engine(string_if_invalid="INVALID(%s)")
    self.assertEqual(engine.from_string("{{ nope }}|{{ nope|upper }}").render(Context()), "INVALID(nope)|INVALID(nope)")
    engine2 = Engine()
    self.assertEqual(engine2.from_string("{{ nope|default:'d' }}").render(Context()), "d")

The second test shows the rules of calling and failing, which the API reference spells out.

  • A callable that raises propagates its exception, unless the exception has silent_variable_failure = True, in which case the variable renders as string_if_invalid. ObjectDoesNotExist (the base of every model’s DoesNotExist) has that attribute, so a missing related object renders as nothing instead of a 500.

  • A callable that needs arguments is not called; the variable renders as string_if_invalid. This is why the template language cannot pass arguments to methods: compute the value in the view or model.

  • A callable with alters_data = True is never called; Django sets that on the generated save() and delete() of models, so a template cannot delete data with {{ book.delete }}. Set it on your own methods that mutate state.

  • A callable with do_not_call_in_templates = True is treated as a plain object, which lets you reach attributes of a class or enum (the TextChoices classes of the models page work this way).

  • A variable or attribute that starts with an underscore is rejected when the template is compiled with a TemplateSyntaxError, because such names are private by convention.

  • Every context contains True, False and None.

  • Unknown variables render as string_if_invalid, empty by default. Filters are only applied to an invalid variable when that option is the empty string, whereas {% if %}, {% for %} and {% regroup %} treat an invalid variable as None. Do not set string_if_invalid as a development default, because Django’s own templates rely on the silence; use it briefly to chase one problem.

A related limit: the language has no escape for its own delimiters. A string literal containing %} or }} inside a tag or filter argument is a syntax error. Output such characters with {% templatetag openblock %} or {% verbatim %}, or put the text in a variable.

Filters, tags and {% load %}

A filter is applied with a pipe, takes zero or one argument after a colon (quote it when it contains spaces) and filters chain left to right: {{ text|escape|linebreaks }}. Django has about sixty built-in filters and about two dozen built-in tags, listed in The built-in tags and filters. Custom tags and filters live in a library that a template must {% load %}; the library’s app must be in INSTALLED_APPS (or registered in the engine’s libraries option). Two rules cause most confusion.

  • {% load %} is not inherited. A parent that loads humanize does not make the filters available to a child that extends it, and the other way round; every template loads what it uses. The builtins option (the scratch project uses it for an initials filter) is the escape hatch for libraries you want everywhere.

  • {% load foo bar from somelibrary %} loads only the named tags and filters.

Filters in practice, the if-expression grammar, and the load rule (excerpts of catalog/tests.py)
def test_filters_and_default(self):
    self.assertEqual(self.render("{{ v|default:'n/a' }}|{{ n|default_if_none:'none' }}|{{ e|default_if_none:'none' }}", v="", n=None, e=""), "n/a|none|")
    self.assertEqual(self.render("{{ 1234567.891|floatformat:'2g' }}|{{ x|length }}", x=[1, 2]), "1,234,567.89|2")
    self.assertEqual(self.render("{{ v|yesno:'yes,no,maybe' }}", v=None), "maybe")


def test_if_precedence_and_none(self):
    self.assertEqual(self.render("{% if a == b or c == d and e %}Y{% else %}N{% endif %}", a=1, b=2, c=3, d=3, e=0), "N")
    self.assertEqual(self.render("{% if missing is None %}none{% endif %}"), "none")
    self.assertEqual(self.render("{% if a > b and b > c %}ok{% endif %}", a=3, b=2, c=1), "ok")


def test_load_not_inherited(self):
    engine = Engine(loaders=[("django.template.loaders.locmem.Loader", {
        "p.html": "{% load humanize %}{% block c %}{{ 1000|intcomma }}{% endblock %}",
        "c.html": "{% extends 'p.html' %}{% block c %}{{ 1000|intcomma }}{% endblock %}",
    })], libraries={"humanize": "django.contrib.humanize.templatetags.humanize"})
    with self.assertRaises(TemplateSyntaxError):
        engine.get_template("c.html").render(Context())


def test_unknown_filter_is_error(self):
    with self.assertRaisesMessage(TemplateSyntaxError, "Invalid filter: 'nope'"):
        Template("{{ x|nope }}")

The {% if %} tag supports and, or, not, in, not in, is, is not, ==, !=, <, >, ⇐, >= and filters, with Python’s precedence (lowest to highest: or, and, not, in, the comparisons). Comparisons cannot be chained (a > b > c is wrong, write a > b and b > c), parentheses are not supported, and because most filters return strings, numeric comparisons on filter output usually do not do what you expect (length is the exception). Writing the tag as {% if somevar is None %} is true both for None and for a name missing from the context.

Template inheritance, include and the base template

Inheritance is the most powerful part of the DTL. A base template defines the skeleton of the site and marks the parts a page may change with {% block name %}; a child template says {% extends "base.html" %} and fills the blocks it cares about. Everything the child leaves undefined falls back to the parent’s content, and {{ block.super }} pulls the parent’s content into the child’s block so that you add to it instead of replacing it (that output is not escaped again, because it was already escaped where it came from).

templates/base.html
{% load static %}<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>{% block title %}{{ SITE_NAME }}{% endblock title %}</title>
  <link rel="stylesheet" href="{% static 'catalog/site.css' %}" {% csp_nonce_attr %}>
</head>
<body>
  <nav>
    {% for name, label in nav_items %}
      <a href="{% url name %}"{% if request.resolver_match.view_name == name %} aria-current="page"{% endif %}>{{ label }}</a>
    {% endfor %}
    {% if user.is_authenticated %}Signed in as {{ user.get_username }}{% else %}Anonymous{% endif %}
  </nav>
  {% block content %}{% endblock content %}
  {% include "catalog/footer.html" %}
</body>
</html>
catalog/templates/catalog/book_list.html
{% extends "base.html" %}
{% block title %}Books ({{ page_obj.paginator.count }}) | {{ block.super }}{% endblock %}
{% block content %}
<h1>Books</h1>
<form method="get">
  <input type="search" name="q" value="{{ request.GET.q }}">
  <button>Search</button>
</form>
<ul>
  {% for book in page_obj %}
    {% include "catalog/_book_row.html" with book=book only %}
  {% empty %}
    <li>No books match.</li>
  {% endfor %}
</ul>
<p>Showing {{ page_obj.start_index }}-{{ page_obj.end_index }} of {{ page_obj.paginator.count }}.</p>
{% if page_obj.has_previous %}<a rel="prev" href="{% querystring page=page_obj.previous_page_number %}">Previous</a>{% endif %}
{% if page_obj.has_next %}<a rel="next" href="{% querystring page=page_obj.next_page_number %}">Next</a>{% endif %}
<a href="{% querystring q=None page=None %}">Clear filters</a>
{% endblock content %}
catalog/templates/catalog/_book_row.html
<li>
  <a href="{{ book.get_absolute_url }}">{{ book.title }}</a>
  by {{ book.author.name }} ({{ book.author.name|initials }}), {{ book.price|floatformat:2 }}
</li>
catalog/views.py (the view that renders it)
def book_list(request):
    books = Book.objects.select_related("author")
    q = request.GET.get("q", "").strip()
    if q:
        books = books.filter(Q(title__icontains=q) | Q(author__name__icontains=q))
    page_obj = Paginator(books, 2).get_page(request.GET.get("page"))
    return render(request, "catalog/book_list.html", {"page_obj": page_obj})

The base template is in the project-level templates/ directory because it belongs to the whole project, whereas the page templates belong to the catalog app. The test below drives the whole stack through the test client; curl against runserver shows the same HTML.

Inheritance, include, querystring, context processor and project-directory precedence (excerpt of catalog/tests.py)
def test_inheritance_include_querystring(self):
    r = self.client.get("/books/?q=a&page=2")
    self.assertContains(r, "<h1>Books</h1>", html=True)
    self.assertContains(r, "<title>Books (5) | Bookshelf</title>", html=True)
    self.assertContains(r, 'href="?q=a&amp;page=1"')  # previous link keeps q
    self.assertContains(r, 'href="?q=a&amp;page=3"')  # next link keeps q
    self.assertContains(r, 'href="?"')  # clear filters
    self.assertTemplateUsed(r, "catalog/_book_row.html")
    self.assertContains(r, "(BB)")  # builtins library: initials filter without {% load %}
    self.assertContains(r, "project templates directory")  # DIRS beats APP_DIRS
$ curl -s "http://127.0.0.1:8129/books/?q=a&page=2"
...
<title>Books (5) | Bookshelf</title>
...
<p>Showing 3-4 of 5.</p>
<a rel="prev" href="?q=a&amp;page=1">Previous</a>
<a rel="next" href="?q=a&amp;page=3">Next</a>
<a href="?">Clear filters</a>

The rules worth remembering:

  • {% extends %} must be the first tag of the template; placing a tag before it is a TemplateSyntaxError. It accepts a literal name, a variable holding a name (or a compiled Template), and paths relative to the current template (./base2.html, ../base1.html).

  • A template cannot define two blocks with the same name: a block works in both directions, filling the parent’s hole and defining the content for the grandchild’s.

  • Blocks are evaluated before the surrounding tags, so a {% block %} inside {% if %} is overridden whether or not the condition holds. Put the condition inside the block.

  • Variables created with the as syntax outside a block (for example {% translate "Title" as title %}) are not visible inside a block.

  • Name the closing tag ({% endblock content %}) in long templates; it is checked against the opening tag.

  • A three-level layout (a site base, a section base such as base_news.html, individual pages) maximises reuse, and more blocks in the base template are better than fewer.

{% include %} is the other reuse tool. It renders another template with the current context and inserts the result; the name may be a quoted string, a variable, an object with a render() method or (since Django 3.1) an iterable of names of which the first loadable one is used. with key=value adds variables and only restricts the included template to the variables passed. Treat it as "render a subtemplate and paste the HTML", not as "paste the subtemplate source": there is no shared state between inclusions and blocks inside the included file are already evaluated.

with and only (excerpt of catalog/tests.py)
def test_with_include_only(self):
    out = render_to_string("catalog/_book_row.html", {"book": Book(pk=1, title="T", author=Author(name="Ann Archer"), price=1)})
    self.assertIn("(AA)", out)

Auto-escaping and safe output

Every variable output is HTML-escaped by default: <, >, ', " and & become <, >, ', " and &. That is Django’s main defence against cross-site scripting (XSS), and it is applied after the filters, so filter output is escaped too unless a filter has marked it safe. Escaping is on at three levels, from broad to narrow: the engine (OPTIONS["autoescape"]), a block ({% autoescape off %}) and a variable (|safe).

Where escaping applies and where it does not (excerpts of catalog/tests.py)
def test_autoescape_rules(self):
    self.assertEqual(self.render("{{ v }}", v="<b>"), "&lt;b&gt;")
    self.assertEqual(self.render("{{ v|safe }}", v="<b>"), "<b>")
    self.assertEqual(self.render("{% autoescape off %}{{ v }}{% endautoescape %}", v="<b>"), "<b>")
    self.assertEqual(self.render("{{ v|default:'3 &lt; 2' }}", v=""), "3 &lt; 2")  # literals are not escaped
    # join under autoescape off marks the result safe, so escape is a no-op; escapeseq fixes it
    t = "{% autoescape off %}{{ l|join:', '|escape }}|{{ l|escapeseq|join:', ' }}{% endautoescape %}"
    self.assertEqual(self.render(t, l=["<a>", "<b>"]), "<a>, <b>|&lt;a&gt;, &lt;b&gt;")


def test_escaping(self):
    Book.objects.create(title="<script>alert(1)</script>", author=Author.objects.first(), price=1)
    r = self.client.get("/books/?q=script")
    self.assertContains(r, "&lt;script&gt;alert(1)&lt;/script&gt;")
    self.assertNotContains(r, "<script>alert(1)")


def test_json_script(self):
    out = self.render("{{ data|json_script:'d' }}", data={"hello": "world</script>&"})
    self.assertEqual(out, '<script id="d" type="application/json">{"hello": "world\\u003C/script\\u003E\\u0026"}</script>')

The rules and pitfalls follow from that code.

  • String literals are trusted. A literal in a tag or filter argument (default:"3 < 2") is inserted as if it were passed through safe, because the template author controls it; write it already escaped. Values coming from variables are still escaped.

  • autoescape propagates through {% extends %} and {% include %} just like any block tag: a base template that turns it off turns it off in every child.

  • escape is not force_escape. escape escapes once and does nothing to a value already escaped or marked safe; force_escape always escapes.

  • Chaining after join with escaping off is unsafe. {{ my_list|join:", "|escape }} under autoescape off outputs the elements raw, because join marks its result safe and escape then leaves it alone. Escape each element with escapeseq (new in Django 5.0) before joining; safeseq is its opposite. Anything applied after safe can make the value unsafe again, so |safe|escape prints the raw value.

  • Data for JavaScript must not be written into script code with escapejs for anything other than a quoted string literal (and never inside a template literal). Use json_script (the id argument became optional in Django 4.1) or data- attributes: the filter escapes <, > and & and works under a strict Content Security Policy.

  • autoescape off is for non-HTML engines. If you render plain-text emails, add a separate engine with autoescape: False rather than turning it off in blocks of an HTML template.

  • Never let users supply template source. The documentation warns that the template system is not safe against untrusted template authors, who can run XSS or read attributes of context variables. Compile only templates you wrote.

The built-in tags and filters

The built-in reference is long but regular. The first table lists every built-in tag, the second every built-in filter; the three libraries that need {% load %} come last. The sections after the tables go deeper on the tags that cause the most questions.

Tag Use Since

autoescape

Turns auto-escaping on or off for a block (on or off).

block, extends

Template inheritance; extends takes a literal, a variable or a relative path.

comment

Multi-line comment with an optional note; cannot be nested.

csrf_token

Outputs the hidden CSRF field inside every POST form; needs the request in the context (see csrf_token and the request).

csp_nonce_attr

Renders nonce="…​" for <script> and <link> elements, or the assets of a Media object with the nonce applied.

6.1

cycle, resetcycle

cycle yields its arguments in turn (row striping); as name names a cycle; resetcycle restarts it.

debug

Dumps the context and imported modules; outputs nothing when DEBUG is False.

filter

Applies filters to a whole block; escape and safe are not allowed here.

firstof

First argument that is not false; optional literal fallback; as var stores it; auto-escapes.

for, empty

Loops; unpacking (for k, v in data.items), reversed, the forloop variable (see below), and an {% empty %} clause.

if, elif, else

Conditions with boolean operators, comparisons, in, is, and filters.

ifchanged

Inside a loop: output only when a value (or the block’s own output) changed since the last iteration; optional else.

include

Renders another template with the current context; with adds variables, only isolates.

load

Loads tag libraries, optionally from a library.

lorem

Placeholder Latin text: count, method (w, p, b) and random.

now

Current date and time with a date format string, or a predefined one such as SHORT_DATETIME_FORMAT; supports as var.

partialdef, partial

Defines and renders a named fragment inside a template; inline renders it where defined.

6.0

querystring

Builds a URL-encoded query string from request.GET or given mappings (see below).

5.1

regroup

Groups a list of similar objects by an attribute; does not sort.

spaceless

Removes whitespace between HTML tags (not within text).

templatetag

Outputs one of the template delimiters (openblock, closeblock, openvariable, closevariable, openbrace, closebrace, opencomment, closecomment).

url

Reverses a named URL pattern; as var suppresses the NoReverseMatch error; supports namespaces.

verbatim

Stops rendering inside the block; an optional name lets the block contain {% endverbatim %}.

widthratio

Scales a value against a maximum to a constant (bar charts); supports as var.

with

Caches a value under a short name for the block: {% with total=business.employees.count %}.

Group Filters

Text and case

capfirst, lower, upper, title, cut, slugify, truncatechars, truncatechars_html, truncatewords, truncatewords_html, wordcount, wordwrap, center, ljust, rjust, linenumbers, stringformat, phone2numeric, addslashes

HTML, escaping and safety

escape, force_escape, escapeseq, safe, safeseq, striptags, linebreaks, linebreaksbr, urlize, urlizetrunc, escapejs, json_script, iriencode, urlencode

Numbers

add, divisibleby, floatformat, filesizeformat, get_digit, pluralize

Dates and times

date, time, timesince, timeuntil

Lists and dictionaries

first, last, join, length, slice, random, make_list, dictsort, dictsortreversed, unordered_list

Defaults and logic

default, default_if_none, yesno

Debugging

pprint

Library (load name) Contents

{% load static %}

static (URL of a static file through the staticfiles storage, as var supported), get_static_prefix, get_media_prefix. Covered on Static Files & Front-End Assets.

{% load i18n %}, {% load l10n %}, {% load tz %}

Translation (translate, blocktranslate), localisation and time-zone tags and filters; see Internationalization & Translation and Localization & Time Zones.

{% load humanize %}

intcomma, intword, naturalday, …​ from django.contrib.humanize, which must be in INSTALLED_APPS.

for, forloop and empty

{% for %} sets a forloop variable inside the loop: counter (1-based), counter0, revcounter, revcounter0, first, last, parentloop (for nested loops) and, new in Django 6.0, length, the number of items in the loop. Before 6.0 you had to apply the length filter to the sequence; forloop.length makes "item 3 of 12" a one-liner that also works for any iterable the loop already holds. The {% empty %} clause replaces the common {% if list %} wrapper.

catalog/templates/catalog/loop_demo.html
{% for book in books %}{{ forloop.counter }}/{{ forloop.length }}{% if forloop.first %} first{% endif %}{% if forloop.last %} last{% endif %}: {{ book }}
{% endfor %}
forloop in a test (excerpt of catalog/tests.py)
def test_forloop(self):
    out = render_to_string("catalog/loop_demo.html", {"books": ["a", "b", "c"]})
    self.assertEqual(out.split(), ["1/3", "first:", "a", "2/3:", "b", "3/3", "last:", "c"])

For a sequence of pairs, unpack in the tag ({% for key, value in data.items %}); when a dictionary has a key called items, the lookup order of Variables and the dot lookup bites again.

querystring: links that keep the current filters

{% querystring %} (Django 5.1) outputs a URL-encoded query string, starting from request.GET unless you pass mappings of your own. It removes the hand-written query-string surgery that every paginated and filtered list used to need.

Usage Result for a request with ?color=green&size=M

{% querystring %}

?color=green&size=M, the query string as it is (an empty query string gives ?)

{% querystring size="L" %}

?color=green&size=L: keyword arguments add or replace

{% querystring color=None %}

?size=M: None removes the parameter

{% querystring color=my_list %}

with ["red", "blue"]: ?color=red&color=blue&size=M; lists are preserved as repeated keys

{% querystring page=page_obj.next_page_number %}

the pagination idiom: everything else is kept

{% querystring my_dict page=2 as next_url %}

positional arguments must be mappings (QueryDict or dict) and replace the request.GET default; later arguments win; as var stores the result

Two changes arrived in Django 6.0: the result now always begins with ?, even when it would otherwise be empty (an empty href reloads nothing in some browsers), and several positional mappings are accepted. To use request.GET as the default the django.template.context_processors.request processor must be enabled (or pass request/a QueryDict yourself). The list template above uses it for previous, next and "clear filters" links, and the test of the inheritance section checks that the search term survives paging. Since the output is a query string only, the link is relative to the current path; add {{ request.path }} in front if the link must be absolute-path.

regroup, ifchanged, cycle, url and static in brief

  • {% regroup cities by country as country_list %} produces objects with grouper and list. It does not sort, so unsorted input yields one group per run: sort in the view or use dictsort (regroup cities|dictsort:"country" by country). Any lookup is a valid grouper, including get_country_display for a choices field.

  • {% ifchanged %} inside a loop is the standard way to print a heading once per month or per author; {% cycle %} and {% resetcycle %} handle zebra striping that restarts per group.

  • {% url 'catalog:book-detail' book.pk %} reverses by name and never hardcodes a path; a missing name raises NoReverseMatch, a missing view with the as form does not. Quote the name: an unquoted name is a variable. Do not mix positional and keyword arguments. See URLs & Routing.

  • {% static 'catalog/site.css' %} goes through the staticfiles storage, so it returns hashed names when a manifest storage is configured (see Static Files & Front-End Assets).

  • {% firstof a b "fallback" %}, {% with %} and {% widthratio %} all accept as to capture their output; with is how to evaluate an "expensive" attribute once ({% with total=business.employees.count %}), but a value that costs a query belongs in the view.

regroup needs sorted input (excerpt of catalog/tests.py)
def test_regroup_needs_sorted_input(self):
    rows = [{"a": "Ann"}, {"a": "Bob"}, {"a": "Ann"}]
    t = "{% regroup rows by a as g %}{% for x in g %}{{ x.grouper }}={{ x.list|length }} {% endfor %}"
    self.assertEqual(self.render(t, rows=rows), "Ann=1 Bob=1 Ann=1 ")
    t2 = "{% regroup rows|dictsort:'a' by a as g %}{% for x in g %}{{ x.grouper }}={{ x.list|length }} {% endfor %}"
    self.assertEqual(self.render(t2, rows=rows), "Ann=2 Bob=1 ")

Partials in one paragraph

Since Django 6.0 {% partialdef name %}…​{% endpartialdef %} defines a named fragment of a template, {% partial name %} renders it (as often as you like, with the current context), the inline option renders it where it is defined, and "template.html#name" addresses it from get_template(), render() and {% include %}. It is the DTL’s answer to "small reusable component without a separate file", and is the natural fit for htmx-style responses. The syntax and the design decisions have their own page, Template Partials & htmx; the check here is that the whole template and one of its partials render as expected.

catalog/templates/catalog/partials_demo.html
{% partialdef card %}<div class="card">{{ book.title }}</div>{% endpartialdef card %}
{% for book in books %}{% partial card %}{% endfor %}
Loading a partial by name (excerpt of catalog/tests.py)
def test_load_a_partial_by_name(self):
    whole = render_to_string("catalog/partials_demo.html", {"books": [Book(title="A"), Book(title="B")]})
    only = render_to_string("catalog/partials_demo.html#card", {"book": Book(title="C")})
    self.assertEqual(whole.split(), ['<div', 'class="card">A</div><div', 'class="card">B</div>'])
    self.assertEqual(only, '<div class="card">C</div>')

csrf_token and the request

Every POST form rendered by the DTL needs {% csrf_token %}, which outputs the hidden csrfmiddlewaretoken field. The tag works because the csrf context processor is always active for a RequestContext; consequently you must render with the request (render(), or render_to_string(…​, request=request)), otherwise the field is empty and the post is rejected with 403. The detail template of the scratch project contains the tag and the test below renders it with a client that enforces CSRF checks.

catalog/templates/catalog/book_detail.html
{% extends "base.html" %}
{% block title %}{{ book.title }} | {{ block.super }}{% endblock %}
{% block content %}
<h1>{{ book.title }}</h1>
<div class="blurb">{{ book.blurb|linebreaks }}</div>
<p>{{ book.price|floatformat:"2g" }}</p>
<form method="post" action="{% url 'catalog:book-detail' book.pk %}">
  {% csrf_token %}
  <button>Add to cart</button>
</form>
{% endblock content %}
The token appears in the rendered form (excerpt of catalog/tests.py)
def test_detail_csrf(self):
    from django.test import Client

    book = Book.objects.get(title="Alpha")
    r = Client(enforce_csrf_checks=True).get(book.get_absolute_url())
    self.assertContains(r, 'name="csrfmiddlewaretoken"')
    self.assertContains(r, "<p>9.50</p>", html=True)
    self.assertContains(r, "<p>Blurb Alpha<br>second line</p>")

CSRF protection as a whole is covered on Security; the Jinja2 backend provides csrf_input and csrf_token instead (see The Jinja2 backend).

Context processors

A context processor is a plain function that takes the current HttpRequest and returns a dictionary, which is merged into the context of every template rendered with a request. It is how user, perms, messages and request reach every template without each view passing them. The processors are listed in OPTIONS["context_processors"] and are applied in order, so a later processor overrides an earlier one that produced the same name.

The context of a RequestContext is built in layers, and the order matters:

  1. The processors run on top of the base context. A processor can therefore overwrite a variable you passed.

  2. The exception is the shortcuts: render() and TemplateResponse push your context above the processors, so data you pass explicitly wins. Code that builds a RequestContext by hand gets the opposite order unless it calls push().

  3. django.template.context_processors.csrf is always on and cannot be turned off, whatever the option says.

Processor Adds to the context Note

django.template.context_processors.request

request

Required by {% querystring %} defaults and by the admin.

django.contrib.auth.context_processors.auth

user (a User or AnonymousUser) and perms (a PermWrapper)

Required by the admin.

django.contrib.messages.context_processors.messages

messages and DEFAULT_MESSAGE_LEVELS

Required by the admin.

django.template.context_processors.debug

debug and sql_queries, only when DEBUG is True and the client address is in INTERNAL_IPS

No longer in the default project template since 5.2.

django.template.context_processors.i18n

LANGUAGES, LANGUAGE_BIDI, LANGUAGE_CODE

django.template.context_processors.tz

TIME_ZONE, the name of the active zone

django.template.context_processors.static

STATIC_URL

django.template.context_processors.media

MEDIA_URL

django.template.context_processors.csrf

the token used by {% csrf_token %}

Always enabled.

django.template.context_processors.csp

csp_nonce, a per-request nonce for a Content Security Policy

New in 6.0; feeds {% csp_nonce_attr %} (6.1).

Writing one takes five lines. The scratch project’s site_info processor supplies the site name, a debug flag and the navigation entries used by the base template.

catalog/context_processors.py
from django.conf import settings


def site_info(request):
    return {
        "SITE_NAME": "Bookshelf",
        "DEBUG_MODE": settings.DEBUG,
        "nav_items": [
            ("catalog:book-list", "Books"),
        ],
    }
Processors run only with a request, and view data wins (excerpts of catalog/tests.py)
def test_request_context_applies_processors(self):
    request = RequestFactory().get("/")
    request.user = AnonymousUser()
    t = Template("{{ SITE_NAME }}|{{ request.path }}|{{ user.is_authenticated }}")
    self.assertEqual(t.render(RequestContext(request)), "Bookshelf|/|False")


def test_view_context_beats_processor(self):
    request = RequestFactory().get("/")
    request.user = AnonymousUser()
    out = render_to_string("catalog/footer.html", {"SITE_NAME": "View wins"}, request=request)
    self.assertIn("View wins", out)


def test_without_request_no_processors(self):
    # no request, no RequestContext, no context processors: SITE_NAME is simply missing
    self.assertTrue(render_to_string("catalog/footer.html").startswith("<footer> ("))

Pitfalls specific to processors:

  • They run on every render that has a request, including admin pages and error pages, so keep them cheap. A processor that runs a query pays for it on pages that never show the result; return a lazy value (a SimpleLazyObject, a queryset not yet evaluated) or compute the data in a template tag instead.

  • Names are global. Prefix keys (SITE_NAME, not name) to avoid collisions with view context.

  • render_to_string("x.html", {…​}) without a request does not run processors; neither does a call to Template.render(Context(…​)). Emails rendered in background tasks therefore need the values passed explicitly.

  • The Jinja2 backend supports the option but the documentation discourages using it there: put a function in the environment’s globals and call it from the template.

The template API for Python programmers

Underneath the backend-agnostic functions sits the DTL’s own API: Engine (configuration), Template (a compiled template) and Context (the data). Using the system in Python is a three-step process: configure an engine, compile template code into a Template, render it with a Context. The high-level API does all three for you, and in a Django project you should rarely need more than get_template() and render(); the low-level classes matter for libraries, tests and scripts that use the DTL without a project.

Class Notes

Engine(dirs, app_dirs, context_processors, debug, loaders, string_if_invalid, file_charset, libraries, builtins, autoescape)

All arguments are keyword-only; the names mirror the OPTIONS of DjangoTemplates, with different defaults (debug=False, app_dirs=False, autoescape=True). Methods: from_string(), get_template(), select_template() and the static get_default(), which returns the first configured DjangoTemplates engine and exists for APIs that need an implicit global engine; its use elsewhere is discouraged.

Template(template_string)

Compiles once, renders many times; parsing is stored as a tree. Prefer the engine’s factory methods.

Context(dict_, autoescape, use_l10n, use_tz)

A stack of dictionaries. push()/pop() (usable as context managers), update(), flatten(), get(), setdefault(). Popping too far raises ContextPopException.

RequestContext(request, dict_, processors, use_l10n, use_tz, autoescape)

A Context that also runs the context processors; the optional processors argument adds more for this one rendering.

Standalone engine and the context stack (excerpts of catalog/tests.py)
def test_standalone_engine(self):
    engine = Engine(debug=False)
    out = engine.from_string("{{ n|add:1 }} {{ s|title }}").render(Context({"n": 1, "s": "the web"}))
    self.assertEqual(out, "2 The Web")


def test_context_stack(self):
    c = Context({"foo": "first"})
    with c.push(foo="second"):
        self.assertEqual(c["foo"], "second")
    self.assertEqual(c["foo"], "first")
    self.assertEqual(c.flatten()["foo"], "first")

A custom tag that opens a new scope (a block tag that sets variables for its body) pushes a layer with with context.push(…​) so that its variables disappear afterwards; that is the main reason to know the stack. Template syntax errors are detected at compile time, so loading a template is where TemplateSyntaxError surfaces, not rendering:

Compile-time errors (excerpt of catalog/tests.py)
def test_block_super_and_syntax_error(self):
    with self.assertRaises(TemplateSyntaxError):
        Template("{% if %}x{% endif %}")
    with self.assertRaises(TemplateSyntaxError):
        Template("{% block a %}{% endblock %}{% block a %}{% endblock %}")


def test_extends_must_be_first(self):
    with self.assertRaisesMessage(TemplateSyntaxError, "must be the first tag"):
        Template("{% if x %}y{% endif %}{% extends 'base.html' %}")
    Template("hello{% extends 'base.html' %}")  # plain text before it is tolerated

Template loaders and caching

A loader finds a template by name, reads it and returns a Template. The loaders option lists them; Django tries each in order and stops at the first match.

Loader Behaviour Default

django.template.loaders.filesystem.Loader

Searches DIRS. Finds nothing until DIRS is non-empty. Can be given its own directory list as a tuple argument.

on

django.template.loaders.app_directories.Loader

Searches the templates subdirectory of each installed app, in INSTALLED_APPS order, and caches which apps have one.

on with APP_DIRS

django.template.loaders.cached.Loader

Wraps other loaders and keeps the compiled Template in memory so each file is read and parsed once.

on when loaders is omitted

django.template.loaders.locmem.Loader

Reads from a dictionary of name to source. Meant for tests.

off

a subclass of django.template.loaders.base.Loader

Your own source: implement get_template_sources() and get_contents().

off

The cached loader was historically enabled only when DEBUG was False. Since Django 4.1 it is also enabled in development when OPTIONS["loaders"] is not specified, and since 3.2 templates it holds are correctly reloaded when their files change in development, so there is no longer a reason to disable it. Specifying loaders yourself turns the automatic wrapping off: you must then wrap in the cached loader explicitly in production. A custom tag whose Node stores per-render state on self is not thread-safe with this loader, because a compiled template is shared by all requests.

Defaults, explicit loaders and the locmem loader (excerpts of catalog/tests.py)
def test_default_loaders_are_cached(self):
    from django.template.loaders.cached import Loader as Cached

    loaders = engines["django"].engine.template_loaders
    self.assertEqual(len(loaders), 1)
    self.assertIsInstance(loaders[0], Cached)
    self.assertEqual(get_template("catalog/footer.html").template, get_template("catalog/footer.html").template)


def test_explicit_loaders_disable_default_cache(self):
    engine = Engine(dirs=["/nonexistent"], loaders=["django.template.loaders.filesystem.Loader"])
    self.assertEqual(engine.template_loaders[0].__class__.__module__, "django.template.loaders.filesystem")


def test_locmem_loader_for_tests(self):
    with override_settings(
        TEMPLATES=[
            {
                "BACKEND": "django.template.backends.django.DjangoTemplates",
                "OPTIONS": {
                    "loaders": [("django.template.loaders.locmem.Loader", {"index.html": "hi {{ who }}"})]
                },
            }
        ]
    ):
        self.assertEqual(render_to_string("index.html", {"who": "you"}), "hi you")

Source: Loader types

A custom loader

A loader’s get_template_sources(template_name) yields Origin objects for every place the name could live (it need not check existence, but must keep the name safe, as the filesystem loader does for paths), and get_contents(origin) returns the source or raises TemplateDoesNotExist. The base class implements get_template() on top of them. The loader below serves blurbs/<pk>.txt from the blurb column of the Book table; it is wrapped in the cached loader, so each name is read once per process.

catalog/loaders.py
from django.template import Origin, TemplateDoesNotExist
from django.template.loaders.base import Loader as BaseLoader

from .models import Book


class BlurbLoader(BaseLoader):
    """Serves 'blurbs/<pk>.txt' straight from the database."""

    def get_template_sources(self, template_name):
        if template_name.startswith("blurbs/"):
            yield Origin(name=f"db:{template_name}", template_name=template_name, loader=self)

    def get_contents(self, origin):
        pk = origin.template_name.removeprefix("blurbs/").removesuffix(".txt")
        try:
            return Book.objects.get(pk=pk).blurb
        except (Book.DoesNotExist, ValueError):
            raise TemplateDoesNotExist(origin)
Using it (excerpt of catalog/tests.py)
@override_settings(
    TEMPLATES=[
        {
            "BACKEND": "django.template.backends.django.DjangoTemplates",
            "OPTIONS": {
                "loaders": [
                    ("django.template.loaders.cached.Loader", ["catalog.loaders.BlurbLoader"]),
                ]
            },
        }
    ]
)
class CustomLoaderTests(TestCase):
    def test_database_loader(self):
        make_books()
        book = Book.objects.get(title="Beta")
        out = render_to_string(f"blurbs/{book.pk}.txt")
        self.assertEqual(out, "Blurb Beta\nsecond line")
        with self.assertRaises(TemplateDoesNotExist):
            get_template("blurbs/999.txt")

Loading templates from the database is rarely a good idea in production (it ties rendering to the database, defeats code review and version control for templates and is a stored-template-injection risk when users can edit the rows), but a loader is the right extension point for templates in a zip file, a package resource or a remote store. All loaders must set origin.loader, because the cached loader requires it.

Overriding templates

You can replace a template that comes from another app, a third-party package or django.contrib.admin, without touching that app’s files. The mechanism is the loader order: a template with the same name in a directory that is searched earlier shadows the original.

  • From the project templates/ directory. With DIRS = [BASE_DIR / "templates"] and APP_DIRS = True, DIRS is searched before APP_DIRS, so templates/blog/post.html replaces the blog/post.html of the blog app. This is the usual way to override third-party templates.

  • From an app’s templates/ directory. Also possible, but the app must come before the overridden app in INSTALLED_APPS, because the app-directories loader returns the first match. To override the admin from an app, list your app above django.contrib.admin (the scratch project does).

  • Extending the overridden template. Because the loader skips the template it is currently overriding when it resolves {% extends %}, an override can extend the original under the same name and replace only a block, keeping the rest with {{ block.super }}. That is the technique with the smallest maintenance burden.

The scratch project overrides two things. The admin’s base_site.html gains a badge in the branding block, and the project’s catalog/footer.html shadows the version shipped inside the catalog app.

templates/admin/base_site.html
{% extends "admin/base_site.html" %}
{% block branding %}
  <p class="bookshelf-badge">Bookshelf staff</p>
  {{ block.super }}
{% endblock %}
The two footers
{# catalog/templates/catalog/footer.html (app default) #}
<footer>{{ SITE_NAME }} (default footer from the catalog app)</footer>

{# templates/catalog/footer.html (project override, the one that is used) #}
<footer>{{ SITE_NAME }} (footer from the project templates directory)</footer>
Override tests (excerpts of catalog/tests.py)
def test_overriding_project_beats_app(self):
    origin = get_template("catalog/footer.html").origin
    self.assertTrue(origin.name.endswith("templates/catalog/footer.html"))
    self.assertNotIn("/catalog/templates/", origin.name)


def test_extend_overridden_template(self):
    self.assertIn("Bookshelf staff", render_to_string("admin/base_site.html", {"title": "x", "site_title": "S", "site_header": "H"}))
$ curl -s http://127.0.0.1:8129/admin/login/ | grep -n bookshelf
42:  <p class="bookshelf-badge">Bookshelf staff</p>

Pitfalls: an override that copies the whole original template freezes it at one Django version and silently misses later fixes (the admin templates change between releases), so extend instead of copy; an override placed in a directory that is not in DIRS or an installed app is simply never found, and nothing warns you; and when two apps ship the same template path, the one listed first in INSTALLED_APPS wins, which can change when someone reorders the list. Form widgets are overridden in a different way, with the form renderer, covered on Form Rendering & Widgets; admin templates are covered on Admin Customization.

The Jinja2 backend

Django ships a second backend for Jinja2, the other widely used Python template language. It needs pip install Jinja2 (3.1.6 in the scratch project; Django 4.2 raised the minimum to 2.11.0). Set BACKEND to django.template.backends.jinja2.Jinja2; with APP_DIRS it looks in each app’s jinja2/ subdirectory.

The key option is environment: a dotted path to a callable that returns a Jinja2 Environment (default jinja2.Environment), to which Django passes the remaining options as keyword arguments. Django changes a few defaults: autoescape=True, a loader built from DIRS and APP_DIRS, auto_reload=settings.DEBUG, and undefined set to DebugUndefined when DEBUG is on. When a template is rendered with a request, the globals request, csrf_input and csrf_token are added; apart from that the backend is deliberately minimal, knows nothing about Django’s filters and tags, and you wire Django features into the environment yourself.

config/jinja2.py
from django.templatetags.static import static
from django.urls import reverse
from jinja2 import Environment


def environment(**options):
    env = Environment(**options)
    env.globals.update({"static": static, "url": reverse})
    return env
catalog/jinja2/catalog/book_list.html
<h1>{{ site_name }}</h1>
<ul>
{% for book in books %}
  <li><a href="{{ url('catalog:book-detail', args=[book.pk]) }}">{{ book.title }}</a> - {{ "%.2f"|format(book.price) }}</li>
{% endfor %}
</ul>
<form method="post">{{ csrf_input }}<button>Buy</button></form>
Rendering through the Jinja2 engine (excerpts of catalog/tests.py)
def test_jinja2_engine(self):
    make_books()
    out = render_to_string("catalog/book_list.html", {"site_name": "J", "books": Book.objects.all()[:1]}, using="jinja2")
    self.assertIn('<a href="/books/%d/">' % Book.objects.all()[0].pk, out)
    self.assertIn("9.50", out)
    self.assertIn('<h1>J</h1>', out)


def test_jinja2_csrf_and_autoescape(self):
    request = RequestFactory().get("/")
    out = render_to_string("catalog/book_list.html", {"site_name": "<J>", "books": []}, request=request, using="jinja2")
    self.assertIn("&lt;J&gt;", out)
    self.assertIn('name="csrfmiddlewaretoken"', out)

Because both engines are configured and both contain a template called catalog/book_list.html, the engine list order decides which wins unless using="jinja2" is passed (the test_select_by_engine_order test of Loading and rendering: the engine-agnostic API). In real projects give the two engines different template names or directories to avoid that ambiguity.

The two languages differ in philosophy. Jinja2 lets templates call functions with arguments, evaluate expressions and use tests, so many things that need a custom tag or filter in the DTL are plain function calls; its global namespace removes the need for context processors. The DTL is intentionally restrictive, ships batteries (the admin, form widgets and contrib templates are DTL), integrates tightly with Django features such as {% url %}, {% static %}, {% csrf_token %} and {% translate %}, and is what third-party reusable apps assume.

Aspect Django template language Jinja2

Calling functions with arguments

Not possible; use a tag, filter or compute in the view.

{{ url('catalog:book-detail', args=[book.pk]) }}

Expressions and tests

Limited (if operators, filters).

Full Python-like expressions and is tests.

Django features

Built in.

Wire them in the environment() function.

Context processors

Idiomatic.

Supported but discouraged; use globals.

Reusable apps and admin

DTL only.

Not usable there.

Sandboxing

None; trusted authors only.

SandboxedEnvironment exists, still not a licence to render untrusted templates.

A pragmatic choice: stay with the DTL unless you have a concrete need (speed in a hot path, a team that already knows Jinja2, a templating requirement the DTL cannot express), and if you do mix, keep each language in its own directories. How FastAPI uses Jinja2 directly is covered on FastAPI static files, templates and frontend.

Writing a custom template backend

For a template language Django does not ship, subclass django.template.backends.base.BaseEngine. The contract is small.

  • The engine’s __init__(self, params) receives a dictionary with the keys of the TEMPLATES entry. Copy it, pop OPTIONS, pass the rest to super().init() (which stores name, dirs and app_dirs) and build your underlying engine from the options.

  • app_dirname names the subdirectory searched inside installed apps when APP_DIRS is True. BaseEngine exposes the resulting template_dirs (the DIRS plus the app subdirectories).

  • get_template(template_name) returns a template object or raises TemplateDoesNotExist (pass backend=self, and tried for the debug postmortem) or TemplateSyntaxError. from_string(template_code) is optional.

  • The template object needs render(context=None, request=None); when a request is given it should add request, csrf_input and csrf_token (helpers csrf_input_lazy and csrf_token_lazy live in django.template.backends.utils).

catalog/backends.py: an engine for Python’s string.Template ($name placeholders)
from pathlib import Path
from string import Template as StringTemplate

from django.template import TemplateDoesNotExist, TemplateSyntaxError
from django.template.backends.base import BaseEngine
from django.template.backends.utils import csrf_input_lazy, csrf_token_lazy


class PercentEngine(BaseEngine):
    """A tiny backend based on string.Template ($name placeholders)."""

    app_dirname = "percent"

    def __init__(self, params):
        params = params.copy()
        options = params.pop("OPTIONS").copy()
        super().__init__(params)
        self.prefix = options.pop("prefix", "")

    def from_string(self, template_code):
        try:
            return PercentTemplate(StringTemplate(template_code), self.prefix)
        except ValueError as exc:
            raise TemplateSyntaxError(str(exc))

    def get_template(self, template_name):
        tried = []
        for directory in self.template_dirs:
            path = Path(directory) / template_name
            if path.is_file():
                return self.from_string(path.read_text(encoding="utf-8"))
            tried.append((path, "Source does not exist"))
        raise TemplateDoesNotExist(template_name, tried=tried, backend=self)


class PercentTemplate:
    def __init__(self, template, prefix):
        self.template = template
        self.prefix = prefix

    def render(self, context=None, request=None):
        context = dict(context or {})
        if request is not None:
            context["request"] = request
            context["csrf_input"] = csrf_input_lazy(request)
            context["csrf_token"] = csrf_token_lazy(request)
        return self.prefix + self.template.safe_substitute(context)

It is wired up as the third entry of TEMPLATES (see the settings excerpt above) with its OPTIONS dictionary ({"prefix": "[catalog] "}) and an app_dirname of percent. The test renders through the normal API and shows the postmortem hook.

Using the custom backend (excerpts of catalog/tests.py)
def test_percent_backend(self):
    t = engines["percent"].get_template("catalog/greeting.txt")
    self.assertEqual(t.render({"name": "Ana", "site": "Bookshelf"}), "[catalog] Hello Ana, welcome to Bookshelf.\n")
    self.assertEqual(render_to_string("catalog/greeting.txt", {"name": "Ana", "site": "S"}, using="percent"), "[catalog] Hello Ana, welcome to S.\n")


def test_percent_backend_request_and_missing(self):
    t = engines["percent"].from_string("path=$request")
    self.assertIn("WSGIRequest", t.render({}, request=RequestFactory().get("/")))
    with self.assertRaises(TemplateDoesNotExist) as cm:
        engines["percent"].get_template("zzz.txt")
    self.assertEqual(cm.exception.backend, engines["percent"])

Debug integration is optional but valuable. Raising TemplateDoesNotExist(name, tried=[(origin, status), …​], backend=self) fills the "template postmortem" of the technical 404/500 page; setting a template_debug dictionary on exceptions raised while parsing or rendering (keys name, message, source_lines, line, before, during, after, total, top, bottom) lets the page highlight the offending line; and giving the template an origin object with name, template_name and an optional loader_name makes the template visible to tools such as the Django Debug Toolbar.

"Stupid templates": what stays out of a template

The template language already prevents the worst abuses (no arbitrary Python, no method arguments), but it still lets you write queries and business rules into the presentation layer, because related managers and querysets are callable from a template. The guidance of the books and of the documentation agrees: a template should display data that is already decided.

  • Do not query in a template. author.books.count inside a loop is one query per row; author.books.all in the same loop is another per row. Annotate and prefetch in the view (see Query Optimization & Fetch Modes) and have the template read attributes of objects that are already loaded.

  • Do not aggregate or sort in a template. regroup needs sorted input and dictsort sorts in Python after the query; |length on a queryset evaluates it. Order and count in the database.

  • Keep rules out. {% if user.is_staff and book.price > 20 and not book.sold_out %} is a business rule: expose it as a property of the model, or compute a boolean in the view and pass it in the context (a method that needs the user as an argument cannot be called from a template).

  • Prefer filters to tags. A filter is a pure function of its input, easy to test and to read; a tag is for things that need the parser or the context (block tags, setting variables, inclusion).

  • Do not hide state changes. Set alters_data = True on every model method that mutates anything (Django does so for save()/delete()), so a careless {{ obj.method }} cannot change data when a page renders.

  • Keep templates small and named. Use inheritance for layout, include or partials for fragments, and .html files per component; a 600-line template is a design smell.

The scratch project contains the same page written both ways. The bad version asks the template for counts and books of every author; the good version annotates the count and prefetches the books, so the number of queries no longer depends on the data.

catalog/templates/catalog/stupid_bad.html and stupid_good.html
{% for author in authors %}
{{ author.name }}: {{ author.books.count }} books, {% for b in author.books.all %}{{ b.title }};{% endfor %}
{% endfor %}

{% for author in authors %}
{{ author.name }}: {{ author.book_total }} books, {% for b in author.books.all %}{{ b.title }};{% endfor %}
{% endfor %}
The two views (excerpt of catalog/views.py)
def authors_good(request):
    authors = Author.objects.annotate(book_total=Count("books")).prefetch_related("books")
    return render(request, "catalog/stupid_good.html", {"authors": authors}, content_type="text/plain")


def authors_bad(request):
    return render(request, "catalog/stupid_bad.html", {"authors": Author.objects.all()}, content_type="text/plain")
Counting the queries (excerpt of catalog/tests.py)
class StupidTemplatesTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        make_books()

    def test_bad_hits_db_per_author_in_template(self):
        with self.assertNumQueries(1 + 2 * 2):  # authors + (count + books) per author
            self.client.get("/authors-bad/")

    def test_good_prepares_data_in_view(self):
        with self.assertNumQueries(2):  # annotated authors + one prefetch
            r = self.client.get("/authors-good/")
        self.assertContains(r, "Ann Archer: 2 books")

With two authors the bad page issues five queries (one for the authors, then two per author) and the good page two, and the difference grows linearly with the number of authors. Fetch modes added in Django 6.1 can turn such accidents into an error or fix them automatically; see the query optimisation page above. The same discipline applies to template tags that hit the database: they are called once per use.

Templates and async

Django’s template system is synchronous: get_template(), render(), render_to_string() and Template.render() are ordinary functions, and there is no asynchronous rendering API. That does not stop you from using templates in async views (async def views are supported from Django 3.1), but it has one consequence that surprises people. Rendering a template is CPU-bound work and runs in the event loop thread, which is fine for a typical page. The trap is lazy data: a queryset placed in the context is evaluated while the template iterates over it, which is synchronous database access inside the running event loop, and Django refuses with SynchronousOnlyOperation.

The cure is to evaluate in the async view, with the async ORM API (async for, aget() and the other a-prefixed methods; see Async Support), and pass concrete lists to the template. Both views below render the same template; the first works, the second raises.

catalog/views.py (async excerpts)
async def async_books(request):
    # Evaluate the queryset in async code; templates are synchronous.
    books = [book async for book in Book.objects.select_related("author")]
    return render(request, "catalog/loop_demo.html", {"books": books}, content_type="text/plain")


async def async_books_lazy(request):
    # A lazy queryset would be evaluated by the template, i.e. in sync code.
    return render(request, "catalog/loop_demo.html", {"books": Book.objects.all()}, content_type="text/plain")
Testing both with AsyncClient (excerpt of catalog/tests.py)
class AsyncTemplateTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        make_books()

    async def test_async_view_with_evaluated_data(self):
        r = await AsyncClient().get("/async-books/")
        self.assertEqual(r.status_code, 200)
        self.assertIn("5/5 last", r.content.decode())

    async def test_async_view_lazy_queryset_fails(self):
        from django.core.exceptions import SynchronousOnlyOperation

        with self.assertRaises(SynchronousOnlyOperation):
            await AsyncClient().get("/async-books-lazy/")
$ curl -s http://127.0.0.1:8129/async-books/
1/5 first: Alpha
2/5: Beta
...
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8129/async-books-lazy/
500

Other sync-only touchpoints are the same as elsewhere in an async stack: a context processor that touches the database, a custom tag that queries, and a custom loader that reads a database or the network. Django runs sync views and middleware in a thread pool under ASGI, so these only hurt in async def views; in a sync view the same template works unchanged. For whole-page streaming and server-sent events from async views see Responses, Streaming, CSV & PDF.

Content Security Policy nonces in templates

Django 6.0 added built-in Content Security Policy support, and templates are where the nonce is applied. Enable ContentSecurityPolicyMiddleware, put CSP.NONCE in a directive of SECURE_CSP, add the django.template.context_processors.csp context processor, and then {% csp_nonce_attr %} (new in Django 6.1) renders nonce="<value>" on the element it is placed in; given a Media object it renders the assets with the nonce on each <script> and <link>. Without the processor the tag renders nothing, so the same template works in projects with and without a policy. The tag is already in the scratch project’s base.html:

The nonce appears only when the processor is configured (excerpt of catalog/tests.py)
def test_nonce_attribute_only_with_processor(self):
    from django.utils.csp import CSP

    plain = self.client.get("/books/").content.decode()
    self.assertNotIn("nonce=", plain)
    processors = ["django.template.context_processors.request", "django.contrib.auth.context_processors.auth",
                  "django.contrib.messages.context_processors.messages", "catalog.context_processors.site_info",
                  "django.template.context_processors.csp"]
    templates = [dict(settings.TEMPLATES[0], OPTIONS=dict(settings.TEMPLATES[0]["OPTIONS"], context_processors=processors))] + list(settings.TEMPLATES[1:])
    with override_settings(
        TEMPLATES=templates,
        MIDDLEWARE=settings.MIDDLEWARE + ["django.middleware.csp.ContentSecurityPolicyMiddleware"],
        SECURE_CSP={"default-src": [CSP.SELF], "style-src": [CSP.SELF, CSP.NONCE]},
    ):
        r = Client().get("/books/")  # a new client builds a new middleware chain
    nonce = r.headers["Content-Security-Policy"].split("'nonce-")[1].split("'")[0]
    self.assertIn(f'site.css" nonce="{nonce}">', r.content.decode())

A new test client is built for the second request because a client keeps the middleware chain it built on its first request, so changing MIDDLEWARE with override_settings has no effect on an existing client. Since 6.1 the admin and the other built-in templates add the nonce too (when the processor is configured), and the system check security.W027 warns when CSP.NONCE is used without the processor. The policy itself (directives, report-only mode) is the subject of Content Security Policy.

Testing templates

  • The test client returns a response with response.context (the context, a list of Context objects when several templates rendered) and response.templates. assertTemplateUsed(response, "name") and assertContains(…​, html=True) are the assertions used throughout this page; assertNumQueries guards the "no queries in templates" rule.

  • To test a fragment, render it with render_to_string() or compile it with Template(…​); pass request= when it uses request, user or {% csrf_token %}.

  • Use the locmem loader to make a test independent of the files on disk (the loaders section above), or override_settings(TEMPLATES=…​) to test one configuration.

  • Run manage.py check in CI: the template engine checks are part of it.

  • {% debug %}, the debug context processor and string_if_invalid are interactive aids, not assertions.

The wider testing toolbox is on Testing.

Pitfalls checklist

Symptom Likely cause

TemplateDoesNotExist for a template you can see

The app is not in INSTALLED_APPS, APP_DIRS is False, the directory is not under templates/<appname>/, or the template is not in DIRS. With DEBUG on, the postmortem lists every place searched.

Your override of an app’s template is ignored

INSTALLED_APPS order (app-directories loader returns the first match) or a project directory that is missing from DIRS.

Context variable has the wrong value

A context processor with the same name; remember render() lets view data win, a hand-made RequestContext does not.

Filter or tag is "unknown"

Missing {% load %} in this template (it is not inherited from the parent), or the app providing the library is not installed.

A loop over d.items is empty

The dictionary has an items key, or is a defaultdict; convert it to a dict.

{% if %} comparison is always false

Filters return strings; compare numbers after converting in the view. Comparisons cannot be chained.

HTML printed as text, or script in the page

Text that was already escaped and then marked safe, or |safe on user data. Use format_html()/mark_safe() only in code you wrote.

Block content never shows

{% extends %} not first, duplicate block names, or content outside any block in a child template.

{% url %} raises NoReverseMatch

Unquoted name (it is a variable), a missing argument, a missing namespace, or mixed positional and keyword arguments.

POST rejected with 403 from a template-rendered form

{% csrf_token %} missing, or the page was rendered without a request.

SynchronousOnlyOperation when rendering

A queryset in the context was evaluated by the template in an async view; evaluate it in the view.

Slow page with many queries

Related managers or count called in a loop in the template; annotate and prefetch in the view.

Changes to a template do not appear

A production-style cached loader without a restart, or a browser cache; in development the cached loader reloads on change.

Where the books are outdated

The four reference books were written for Django 1.11 to 2.x (and a Django 5 edition exists for the first of them); the template syntax they teach still works, but several surrounding practices have moved.

  • Static files in templates. Examples using {% load staticfiles %} or {% load admin_static %} stop working: both libraries were removed in Django 3.0, and the tag is {% load static %} then {% static "path" %}.

  • ifequal and ifnotequal. Older tutorials use them; they were deprecated in 3.1 and removed in 4.0. Use {% if a == b %}. Likewise the length_is filter was deprecated in 4.2 and removed in 5.1; write {% if list|length == 3 %}.

  • render_to_response(). Deprecated in 2.0 and removed in 3.0. Use render(request, template, context), which also runs context processors; render_to_response(…​, context_instance=RequestContext(request)) is long gone.

  • trans and blocktrans. The tags were renamed translate and blocktranslate in 3.1; the old names still work and the release notes say they will be kept for the foreseeable future, so books that use them are not wrong, merely dated.

  • user.is_authenticated(). In templates the parentheses never mattered, but in code the method form stopped working in 2.0; is_authenticated is a property.

  • The debug context processor in the default TEMPLATES. Older project templates list it; Django 5.2 removed it from the generated settings (it is still available).

  • Template settings. The ancient TEMPLATE_DIRS, TEMPLATE_LOADERS and TEMPLATE_CONTEXT_PROCESSORS settings have long been replaced by the single TEMPLATES list; the books for Django 1.11 and later already use it.

  • Form rendering through templates. Django 4.0 moved form rendering to the template engine and 4.1 added <div>-based form templates, so tutorials that loop over form.field by hand or render {{ form.as_p }} everywhere are older than the current approach, covered on Form Rendering & Widgets.

  • Jinja2 minimum. Django 4.2 needs Jinja2 2.11.0 or newer; the books that mention the Jinja2 backend predate that.

  • "Stupid templates" and DTL versus Jinja2. The advice stands (keep templates dumb, prefer filters over tags), but the book’s reasons for Jinja2’s speed advantage are weaker today: the cached loader is on by default, including in development since 4.1.

What changed recently

Version Change

3.1

{% include %} accepts an iterable of template names; translate/blocktranslate introduced; ifequal deprecated.

3.2

Cached-loader templates reload correctly in development.

4.0

ifequal/ifnotequal removed; forms render through the template engine.

4.1

The cached loader is enabled in development when loaders is not specified; json_script no longer needs an element id; <div>-based form templates and FORM_RENDERER form templates.

4.2

Jinja2 minimum raised to 2.11.0; length_is deprecated.

5.0

escapeseq filter; field groups for form fields.

5.1

{% querystring %}; template engines implement check(); custom tags may set extra data on the Parser that appears on the Template; length_is removed.

5.2

simple_block_tag() for custom tags; the debug context processor is no longer in the default project template.

6.0

Template partials ({% partialdef %}, {% partial %}, template#partial); forloop.length; {% querystring %} always prefixes ? and accepts several mappings; the csp context processor; urlize and urlizetrunc deprecate the HTTP default scheme (see below).

6.1

{% csp_nonce_attr %}; the built-in templates add CSP nonces when the processor is configured; the undocumented parse_bits() helper loses its takes_context argument.

One deprecation needs action: urlize and urlizetrunc link text without a scheme (for example www.djangoproject.com) with http:// today, and the default will change to https:// in Django 2028 (the calendar-versioned release of January 2028). Setting the transitional URLIZE_ASSUME_HTTPS = True opts in during the 6.x cycle. Looking ahead, 6.2 LTS is due in April 2027 and the 5.2 LTS receives security fixes until April 2028; running tests with -W error::DeprecationWarning (or the RemovedInDjango2028Warning family) shows what your templates and tags still rely on.

Summary

Configure TEMPLATES with a project-level directory in DIRS, APP_DIRS = True and the default cached loader; namespace every app’s templates in a subdirectory; build pages from one base template, {% block %} and {% include %} or partials; leave auto-escaping on; supply site-wide data through a small, cheap context processor; override other apps' templates by extending them from DIRS; keep queries, aggregation and rules in views and models so templates stay stupid; evaluate querysets before rendering in async views; and reach for Jinja2 or a custom backend only when you can name the need. The custom tags, partials, static-file and form pages of this section build on exactly these mechanics.