The Django template engine turns plain text files (usually HTML) plus a context of Python data into the finished page your users see. It is the T in Django's MVT (Model-View-Template) pattern, and it ships with the framework — there's nothing extra to install.
This guide covers the engine and its fundamentals for Django 5.x in 2026: how you configure it, how templates are found and rendered, the core Django Template Language (DTL) building blocks, template inheritance, static files, and when you might pick Jinja2 instead. For a focused tour of the syntax itself, see our Django Template Language intro.
Under the hood the engine does two things: it compiles a template into a reusable object once, then renders that object against a context dictionary to produce a string. In everyday code you rarely touch that low-level API — the render() shortcut does both steps — but knowing what happens underneath makes the rest click.
Need a team to build or modernize a Django app end to end? See our Django development services.
Configuring the engine: the TEMPLATES setting
Every project configures its template engines in the TEMPLATES list in settings.py. Each entry describes one engine; a fresh startproject gives you a single DjangoTemplates backend. The options that matter:
BACKEND— the engine class.django.template.backends.django.DjangoTemplatesis the built-in DTL engine;django.template.backends.jinja2.Jinja2is the bundled Jinja2 backend.DIRS— project-wide directories to search, in order. UseBASE_DIR / "templates"for templates that aren't tied to one app.APP_DIRS— whenTrue, the engine also looks in atemplates/subfolder inside every installed app. This is what makes app-local templates "just work".OPTIONS— backend-specific settings; for DTL the important one iscontext_processors.context_processors— callables that inject variables into everyRequestContext. The defaults give yourequest,user/perms(auth),messages, and debug data. Themessagesprocessor is what powers the Django messages framework.
# settings.py (Django 5.x)
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"], # project-wide templates
"APP_DIRS": True, # also search <app>/templates/
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"django.template.context_processors.debug",
],
},
},
]How templates are discovered and loaded
When you ask for a template by name, the engine walks its loaders in order and returns the first match:
- Each path in
DIRS, top to bottom. - If
APP_DIRSisTrue, thetemplates/folder of each app inINSTALLED_APPS, in order.
Because the first match wins, a strong convention is to namespace app templates: store polls/templates/polls/index.html rather than polls/templates/index.html. Without that extra polls/ folder, two apps that both ship an index.html would clash and the wrong one could be served.
Compiled templates are cached after first load (the cached loader is enabled automatically when APP_DIRS is on and DEBUG is False), so a template compiles once per process, not once per request.
Rendering from a view: the render() shortcut
In a view you almost always use django.shortcuts.render(). It finds the template, builds a RequestContext (so context processors run and csrf_token, request, user, and messages are available), renders it, and returns an HttpResponse:
# views.py
from django.shortcuts import render
def article_list(request):
context = {
"section": {"title": "Latest Stories"},
"story_list": Story.objects.published(),
}
return render(request, "news/article_list.html", context)Rendering outside a view
Sometimes you need the rendered output without an HttpResponse — for an email body or a background job, say. Use render_to_string(), or drop to the low-level Template/Context API. Note that a plain Context does not run context processors (only RequestContext, which render() uses, does):
from django.template.loader import render_to_string
from django.template import Template, Context
# High-level: find a configured template by name and render to a string
html = render_to_string("emails/welcome.html", {"user": user})
# Low-level: compile a raw string and render it against a plain Context
tpl = Template("Hi {{ name }}, welcome aboard!")
out = tpl.render(Context({"name": "Asha"}))The Django Template Language (DTL)
A template is plain text with two special constructs — variables and tags — optionally modified by filters.
Variables use {{ ... }} and are replaced by their value:
{{ section.title }}
The dot is a lookup, attempted in this order: dictionary lookup (section["title"]), then attribute or method lookup (section.title, called if it's callable), then numeric index (mylist.0). The first that succeeds wins, and a missing variable renders as an empty string by default (configurable via string_if_invalid).
Tags use {% ... %} and drive logic or output. The ones you'll reach for constantly:
{% if %}/{% elif %}/{% else %}/{% endif %}— conditionals.{% for x in items %}…{% empty %}…{% endfor %}— loops, with an{% empty %}branch and aforloophelper (forloop.counter,forloop.counter0,forloop.first,forloop.last,forloop.revcounter).{% url "route-name" arg %}— reverse a URL by name instead of hard-coding paths.{% csrf_token %}— required inside every POST<form>.{% with total=order.items.count %}…{% endwith %}— cache an expensive value under a local name.{% now "Y" %}— output the current date/time.
Filters use | to transform a value for display and can take an argument after a colon:
{{ name|lower }}
{{ bio|truncatewords:30 }}
{{ value|default:"nothing" }}
{{ published_at|date:"j M Y" }}
{{ items|length }}
Comments are {# inline #} for one line or {% comment %}…{% endcomment %} for a block.
DTL deliberately ships a small, safe vocabulary; for anything more you write custom tags and filters (covered below). For an example-driven tour of the syntax, see the Django Template Language intro.
{% load static %}
<form method="post" action="{% url 'news:subscribe' %}">
{% csrf_token %}
<input type="email" name="email">
<button>Subscribe</button>
</form>
<h1>{{ section.title }}</h1>
<ul>
{% for story in story_list %}
<li>
<a href="{% url 'news:detail' story.id %}">{{ story.headline|title }}</a>
<small>{{ story.published_at|date:"j M Y" }}</small>
<p>{{ story.summary|truncatewords:25 }}</p>
</li>
{% empty %}
<li>No stories yet.</li>
{% endfor %}
</ul>
<p>© {% now "Y" %} Acme News</p>Autoescaping and XSS safety
By default DTL autoescapes every variable: <, >, ", ', and & become HTML entities before output. This secure-by-default behavior stops user-supplied data from injecting markup or scripts (cross-site scripting, XSS).
Mark content safe only when you genuinely trust it:
{{ trusted_html|safe }}
{% autoescape off %}{{ snippet }}{% endautoescape %}
In Python, wrap known-safe strings with django.utils.safestring.mark_safe, and prefer format_html() so the dynamic parts are still escaped. Rule of thumb: never pass raw user input through safe or mark_safe.
Template inheritance
Inheritance is the engine's most powerful feature. A base template defines the page skeleton and marks overridable regions with {% block %}; child templates fill those regions after declaring {% extends %}.
{# templates/base.html #}
<!doctype html>
<html lang="en">
<head>
<title>{% block title %}Acme{% endblock %}</title>
</head>
<body>
<header>{% include "partials/nav.html" %}</header>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>{# templates/news/article_list.html #}
{% extends "base.html" %}
{% block title %}{{ section.title }} – {{ block.super }}{% endblock %}
{% block content %}
<h1>{{ section.title }}</h1>
{% include "news/_story_list.html" with stories=story_list %}
{% endblock %}{% extends "base.html" %} must be the first tag in a child template. Each {% block %} the child redefines replaces the parent's version; {{ block.super }} pulls in the parent block's original content so you can extend rather than replace it — handy for appending to a title or stacking page-specific assets.
{% include "partials/nav.html" %} renders another template inline, which is the basis for reusable partials and components. Pass data explicitly with with, and isolate the partial from the surrounding context using only:
{% include "news/_card.html" with story=story only %}
Small, focused partials also pair perfectly with HTMX: a view can render just one partial to a string and return that HTML fragment for an AJAX swap instead of re-rendering the whole page.
Serving static files in templates
Don't hard-code asset paths. Load the static tag library and build URLs with {% static %} so they respect STATIC_URL and your storage backend, including hashed, cache-busted names from ManifestStaticFilesStorage:
{% load static %}
<link rel="stylesheet" href="{% static 'css/site.css' %}">
<img src="{% static 'img/logo.svg' %}" alt="Logo">
{% load static %} must appear in each template that uses the tag — it is not inherited from the base template.
DTL vs Jinja2
Django supports both template backends, and you can register them side by side in TEMPLATES. They look alike ({{ }} and {% %}) but differ in philosophy:
- DTL is intentionally restrictive — you can't call arbitrary Python in a template. That keeps presentation logic thin and templates safe by default, and it integrates seamlessly with forms, the admin, and third-party apps (which ship DTL templates). It's the right default for almost every project.
- Jinja2 is faster and more expressive: real expressions, calling functions with arguments, macros, and finer whitespace control. Choose it for very render-heavy pages or when your team already knows Jinja2 — but you give up some Django integration (Jinja2 doesn't consume DTL
context_processorsthe same way, and you register globals likestaticandurlyourself).
A pragmatic setup keeps DTL as the default and adds a Jinja2 backend only for the templates that need its speed. Use distinct directories so each engine resolves its own files:
# settings.py — run both engines side by side
TEMPLATES = [
{
"BACKEND": "django.template.backends.jinja2.Jinja2",
"DIRS": [BASE_DIR / "jinja2"],
"APP_DIRS": True,
"OPTIONS": {"environment": "myproject.jinja2.environment"},
},
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]Extending the engine: custom tags and filters
When the built-in vocabulary isn't enough, DTL is extensible. Register your own filters and tags in an app's templatetags/ package, then {% load %} them. Two common patterns:
- Custom filters and simple tags for small transformations or computed values — see custom template tags and filters.
- Inclusion tags that render a sub-template with their own context — ideal for reusable widgets like menus and cards. See inclusion tags.
Keep heavy logic in views, models, or service functions; use template extensions for presentation only.
Frequently Asked Questions
What is the difference between the Django template engine and the Django template language?
The engine is the machinery that configures, finds, compiles, caches, and renders templates (the TEMPLATES setting, the loaders, and the render() pipeline). The language (DTL) is the syntax — {{ variables }}, {% tags %}, and |filters — that you write inside a template. The engine executes the language.
Where does Django look for my templates?
In the order they're configured: every directory in TEMPLATES["DIRS"] first, then, if APP_DIRS is True, a templates/ folder inside each app in INSTALLED_APPS. The first file matching the requested name wins, which is why you should namespace app templates as app/templates/app/name.html.
Why is my variable showing as blank instead of raising an error?
DTL renders missing or invalid variables as an empty string by default rather than crashing, so a typo or an absent context key silently disappears. Surface these during development by setting the string_if_invalid option, and double-check the variable is actually added to the context in your view.
How do I stop Django from escaping my HTML?
Apply the safe filter ({{ value|safe }}) or wrap a region in {% autoescape off %}…{% endautoescape %}; in Python use mark_safe() or format_html(). Do this only for content you fully trust — passing user input through safe reopens the door to XSS, which autoescaping exists to prevent.
Should I use DTL or Jinja2?
Use DTL for almost everything: it's the default, it's safe and simple, and the admin, forms, and third-party apps depend on it. Reach for Jinja2 only when you need its speed or richer expressions on specific render-heavy pages — and remember you can register both backends in the same project.
Can I reuse a template fragment across several pages?
Yes. Use {% include %} to drop a partial into multiple templates (pass data with with ... only), and use {% extends %} plus {% block %} for whole-page inheritance. For dynamic, reusable widgets, write an inclusion tag — and the same partials can be returned on their own for HTMX-style fragment swaps.