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 |
|
|
Configure where templates live |
|
|
Share a layout between pages |
|
|
Output user data safely |
Do nothing: auto-escaping is on. Reach for |
|
Look up the right tag or filter |
The reference tables list all of them; |
|
Add data to every template |
A context processor listed in |
|
Use templates outside a view or project |
|
|
Change how or where templates are found |
|
|
Replace a template of Django or a third-party app |
Same path in a directory that is searched earlier; |
|
Use Jinja2 or another template language |
Add a second entry to |
|
Keep logic out of templates |
Prepare data in the view or model, keep queries out of loops, prefer filters over tags. |
|
Render under ASGI |
Rendering is synchronous; evaluate querysets in the view before rendering. |
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.
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.
Source: Django: Templates, usage
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 |
|---|---|
|
Dotted path of a class that implements the template backend API. Built in: |
|
The engine’s alias, used with |
|
Directories searched for template sources, in order. Use forward slashes even on Windows. |
|
Whether to search a conventional subdirectory of every installed app: |
|
Backend-specific settings; see the next table. |
The DjangoTemplates backend (a thin wrapper around django.template.Engine) takes these OPTIONS.
| Option | Default | Effect |
|---|---|---|
|
|
HTML auto-escaping of every variable. Set it to |
|
|
Dotted paths of callables that receive the request and return a dictionary merged into the context of requests rendered with a request. |
|
the value of |
Stores extra debug information so the technical 500 page can show the offending template line. |
|
depends on |
List of loader classes (strings, or tuples of class and arguments). When omitted, Django builds filesystem and, with |
|
|
What a misspelled or missing variable renders as. A debugging aid only. |
|
|
Charset used to read template files from disk. |
|
|
Mapping of label to dotted path of a tag library module; the label is what |
|
|
Dotted paths of tag library modules whose tags and filters are available without |
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).
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 inTEMPLATESorder and, inside an engine, every directory inDIRSorder, then every app. The first match wins and the search stops, which is why the DTL engine, listed first, answers forcatalog/book_list.htmleven 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 anindex.htmlshadow each other. -
Mixing
APP_DIRSandloaders.APP_DIRSonly affects the default value ofloaders. Setting both raisesImproperlyConfigured: 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.
Source: Django: Templates, configuration
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 |
|---|---|
|
Finds and returns a template; tries every engine in order unless |
|
Like |
|
Loads and renders in one call. |
|
Mapping of engine alias to configured engine; |
|
The method of the returned object. |
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.
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")
Source: Django: Templates, usage
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 |
|
Looked up in the context, filtered, escaped and output. |
Tag |
|
Control flow, output, loading of libraries. Many are block tags with an |
Filter |
|
Transforms a value; chainable; takes at most one argument. |
Comment |
|
|
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.
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 asstring_if_invalid.ObjectDoesNotExist(the base of every model’sDoesNotExist) 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 = Trueis never called; Django sets that on the generatedsave()anddelete()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 = Trueis treated as a plain object, which lets you reach attributes of a class or enum (theTextChoicesclasses 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,FalseandNone. -
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 asNone. Do not setstring_if_invalidas 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.
Source: Django: Variables and lookups
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 loadshumanizedoes not make the filters available to a child that extends it, and the other way round; every template loads what it uses. Thebuiltinsoption (the scratch project uses it for aninitialsfilter) is the escape hatch for libraries you want everywhere. -
{% load foo bar from somelibrary %}loads only the named tags and filters.
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).
{% 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>
{% 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 %}
<li>
<a href="{{ book.get_absolute_url }}">{{ book.title }}</a>
by {{ book.author.name }} ({{ book.author.name|initials }}), {{ book.price|floatformat:2 }}
</li>
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.
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&page=1"') # previous link keeps q
self.assertContains(r, 'href="?q=a&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&page=1">Previous</a>
<a rel="next" href="?q=a&page=3">Next</a>
<a href="?">Clear filters</a>
Source: Django: Template inheritance
The rules worth remembering:
-
{% extends %}must be the first tag of the template; placing a tag before it is aTemplateSyntaxError. It accepts a literal name, a variable holding a name (or a compiledTemplate), 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
assyntax 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.
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)
Source: Django: the include tag
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).
def test_autoescape_rules(self):
self.assertEqual(self.render("{{ v }}", v="<b>"), "<b>")
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 < 2' }}", v=""), "3 < 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>|<a>, <b>")
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, "<script>alert(1)</script>")
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 throughsafe, because the template author controls it; write it already escaped. Values coming from variables are still escaped. -
autoescapepropagates through{% extends %}and{% include %}just like any block tag: a base template that turns it off turns it off in every child. -
escapeis notforce_escape.escapeescapes once and does nothing to a value already escaped or marked safe;force_escapealways escapes. -
Chaining after
joinwith escaping off is unsafe.{{ my_list|join:", "|escape }}underautoescape offoutputs the elements raw, becausejoinmarks its result safe andescapethen leaves it alone. Escape each element withescapeseq(new in Django 5.0) before joining;safeseqis its opposite. Anything applied aftersafecan make the value unsafe again, so|safe|escapeprints the raw value. -
Data for JavaScript must not be written into script code with
escapejsfor anything other than a quoted string literal (and never inside a template literal). Usejson_script(theidargument became optional in Django 4.1) ordata-attributes: the filter escapes<,>and&and works under a strict Content Security Policy. -
autoescape offis for non-HTML engines. If you render plain-text emails, add a separate engine withautoescape: Falserather 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.
Source: Django: Automatic HTML escaping
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 |
|---|---|---|
|
Turns auto-escaping on or off for a block ( |
|
|
Template inheritance; |
|
|
Multi-line comment with an optional note; cannot be nested. |
|
|
Outputs the hidden CSRF field inside every |
|
|
Renders |
6.1 |
|
|
|
|
Dumps the context and imported modules; outputs nothing when |
|
|
Applies filters to a whole block; |
|
|
First argument that is not false; optional literal fallback; |
|
|
Loops; unpacking ( |
|
|
Conditions with boolean operators, comparisons, |
|
|
Inside a loop: output only when a value (or the block’s own output) changed since the last iteration; optional |
|
|
Renders another template with the current context; |
|
|
Loads tag libraries, optionally |
|
|
Placeholder Latin text: count, method ( |
|
|
Current date and time with a |
|
|
Defines and renders a named fragment inside a template; |
6.0 |
|
Builds a URL-encoded query string from |
5.1 |
|
Groups a list of similar objects by an attribute; does not sort. |
|
|
Removes whitespace between HTML tags (not within text). |
|
|
Outputs one of the template delimiters ( |
|
|
Reverses a named URL pattern; |
|
|
Stops rendering inside the block; an optional name lets the block contain |
|
|
Scales a value against a maximum to a constant (bar charts); supports |
|
|
Caches a value under a short name for the block: |
| Group | Filters |
|---|---|
Text and case |
|
HTML, escaping and safety |
|
Numbers |
|
Dates and times |
|
Lists and dictionaries |
|
Defaults and logic |
|
Debugging |
|
| Library (load name) | Contents |
|---|---|
|
|
|
Translation ( |
|
|
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.
{% for book in books %}{{ forloop.counter }}/{{ forloop.length }}{% if forloop.first %} first{% endif %}{% if forloop.last %} last{% endif %}: {{ book }}
{% endfor %}
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.
Source: Django: the for tag
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 |
|---|---|
|
|
|
|
|
|
|
with |
|
the pagination idiom: everything else is kept |
|
positional arguments must be mappings ( |
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.
Source: Django: the querystring tag
regroup, ifchanged, cycle, url and static in brief
-
{% regroup cities by country as country_list %}produces objects withgrouperandlist. It does not sort, so unsorted input yields one group per run: sort in the view or usedictsort(regroup cities|dictsort:"country" by country). Any lookup is a valid grouper, includingget_country_displayfor 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 raisesNoReverseMatch, a missing view with theasform 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 acceptasto capture their output;withis how to evaluate an "expensive" attribute once ({% with total=business.employees.count %}), but a value that costs a query belongs in the view.
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 ")
Source: Django: the regroup tag
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.
{% partialdef card %}<div class="card">{{ book.title }}</div>{% endpartialdef card %}
{% for book in books %}{% partial card %}{% endfor %}
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>')
Source: Django: Template partials
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.
{% 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 %}
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).
Source: Django: the csrf_token tag
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:
-
The processors run on top of the base context. A processor can therefore overwrite a variable you passed.
-
The exception is the shortcuts:
render()andTemplateResponsepush your context above the processors, so data you pass explicitly wins. Code that builds aRequestContextby hand gets the opposite order unless it callspush(). -
django.template.context_processors.csrfis always on and cannot be turned off, whatever the option says.
| Processor | Adds to the context | Note |
|---|---|---|
|
|
Required by |
|
|
Required by the admin. |
|
|
Required by the admin. |
|
|
No longer in the default project template since 5.2. |
|
|
|
|
|
|
|
|
|
|
|
|
|
the token used by |
Always enabled. |
|
|
New in 6.0; feeds |
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.
from django.conf import settings
def site_info(request):
return {
"SITE_NAME": "Bookshelf",
"DEBUG_MODE": settings.DEBUG,
"nav_items": [
("catalog:book-list", "Books"),
],
}
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, notname) to avoid collisions with view context. -
render_to_string("x.html", {…})without arequestdoes not run processors; neither does a call toTemplate.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.
Source: Django: Using RequestContext
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 |
|---|---|
|
All arguments are keyword-only; the names mirror the |
|
Compiles once, renders many times; parsing is stored as a tree. Prefer the engine’s factory methods. |
|
A stack of dictionaries. |
|
A |
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:
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
Source: Django: Configuring an engine
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 |
|---|---|---|
|
Searches |
on |
|
Searches the |
on with |
|
Wraps other loaders and keeps the compiled |
on when |
|
Reads from a dictionary of name to source. Meant for tests. |
off |
a subclass of |
Your own source: implement |
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.
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.
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)
@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.
Source: Django: Loader types
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. WithDIRS = [BASE_DIR / "templates"]andAPP_DIRS = True,DIRSis searched beforeAPP_DIRS, sotemplates/blog/post.htmlreplaces theblog/post.htmlof theblogapp. 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 inINSTALLED_APPS, because the app-directories loader returns the first match. To override the admin from an app, list your app abovedjango.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.
{% extends "admin/base_site.html" %}
{% block branding %}
<p class="bookshelf-badge">Bookshelf staff</p>
{{ block.super }}
{% endblock %}
{# 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>
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.
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
<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>
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("<J>", 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. |
|
Expressions and tests |
Limited ( |
Full Python-like expressions and |
Django features |
Built in. |
Wire them in the |
Context processors |
Idiomatic. |
Supported but discouraged; use globals. |
Reusable apps and admin |
DTL only. |
Not usable there. |
Sandboxing |
None; trusted authors only. |
|
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.
Source: Django: the Jinja2 backend
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 theTEMPLATESentry. Copy it, popOPTIONS, pass the rest tosuper().init()(which storesname,dirsandapp_dirs) and build your underlying engine from the options. -
app_dirnamenames the subdirectory searched inside installed apps whenAPP_DIRSisTrue.BaseEngineexposes the resultingtemplate_dirs(theDIRSplus the app subdirectories). -
get_template(template_name)returns a template object or raisesTemplateDoesNotExist(passbackend=self, andtriedfor the debug postmortem) orTemplateSyntaxError.from_string(template_code)is optional. -
The template object needs
render(context=None, request=None); when a request is given it should addrequest,csrf_inputandcsrf_token(helperscsrf_input_lazyandcsrf_token_lazylive indjango.template.backends.utils).
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.
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.countinside a loop is one query per row;author.books.allin 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.
regroupneeds sorted input anddictsortsorts in Python after the query;|lengthon 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 = Trueon every model method that mutates anything (Django does so forsave()/delete()), so a careless{{ obj.method }}cannot change data when a page renders. -
Keep templates small and named. Use inheritance for layout,
includeor partials for fragments, and.htmlfiles 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.
{% 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 %}
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")
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.
Source: Django: Accessing method calls
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.
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")
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:
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.
Source: Django: the csp_nonce_attr tag
Testing templates
-
The test client returns a response with
response.context(the context, a list ofContextobjects when several templates rendered) andresponse.templates.assertTemplateUsed(response, "name")andassertContains(…, html=True)are the assertions used throughout this page;assertNumQueriesguards the "no queries in templates" rule. -
To test a fragment, render it with
render_to_string()or compile it withTemplate(…); passrequest=when it usesrequest,useror{% 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 checkin CI: the template engine checks are part of it. -
{% debug %}, thedebugcontext processor andstring_if_invalidare interactive aids, not assertions.
The wider testing toolbox is on Testing.
Pitfalls checklist
| Symptom | Likely cause |
|---|---|
|
The app is not in |
Your override of an app’s template is ignored |
|
Context variable has the wrong value |
A context processor with the same name; remember |
Filter or tag is "unknown" |
Missing |
A loop over |
The dictionary has an |
|
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 |
Block content never shows |
|
|
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 |
|
|
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 |
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" %}. -
ifequalandifnotequal. Older tutorials use them; they were deprecated in 3.1 and removed in 4.0. Use{% if a == b %}. Likewise thelength_isfilter 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. Userender(request, template, context), which also runs context processors;render_to_response(…, context_instance=RequestContext(request))is long gone. -
transandblocktrans. The tags were renamedtranslateandblocktranslatein 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_authenticatedis a property. -
The
debugcontext processor in the defaultTEMPLATES. 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_LOADERSandTEMPLATE_CONTEXT_PROCESSORSsettings have long been replaced by the singleTEMPLATESlist; 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 overform.fieldby 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 |
|
3.2 |
Cached-loader templates reload correctly in development. |
4.0 |
|
4.1 |
The cached loader is enabled in development when |
4.2 |
Jinja2 minimum raised to 2.11.0; |
5.0 |
|
5.1 |
|
5.2 |
|
6.0 |
Template partials ( |
6.1 |
|
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.