Шаблоны Django: синтаксис, наследование и фильтры
Шаблоны Django с примерами: настройка TEMPLATES, переменные, теги if, for, url и static, фильтры, наследование через extends и block, include и свои фильтры.
Содержание · 11 разделов
- Где Django ищет шаблоны: TEMPLATES, DIRS и APP_DIRS
- Переменные и точечная нотация
- Теги: if, for, url, static
- Условия: if, elif, else
- Циклы: for и empty
- Ссылки: url
- Статика: static
- Комментарии и другие теги
- Фильтры
- Наследование шаблонов: extends и block
- Подключение фрагментов: include
- Свои фильтры и теги
- Автоэкранирование и safe
- Частые ошибки
- TemplateDoesNotExist
- Invalid block tag on line N: ‘static’
- Invalid filter: ‘rub’
- Unclosed tag on line N: ‘if’
- Переменная выводится пустой
- Частые вопросы
- Как передать переменную во все шаблоны сразу?
- Как получить значение словаря по ключу из переменной?
- Можно ли считать в шаблоне: складывать, умножать, делить?
- Как безопасно передать данные из шаблона в JavaScript?
- Можно ли использовать Jinja2 вместо шаблонов Django?
- Итог
Шаблоны Django (django templates) — это HTML-файлы со вставками на языке шаблонов Django (DTL). В них три конструкции: {{ переменная }} выводит значение, {% тег %} управляет логикой (условия, циклы, ссылки), а фильтр через | преобразует значение: {{ post.title|upper }}. View передаёт данные в шаблон функцией render():
from django.shortcuts import render
def post_list(request):
return render(request, "blog/post_list.html", {"posts": Post.objects.all()})
Ниже — где Django ищет шаблоны, синтаксис переменных, тегов и фильтров, наследование через extends и block, подключение кусков через include, свои фильтры и автоэкранирование HTML. Примеры проверены на Django 5.2 и 6.x.
Где Django ищет шаблоны: TEMPLATES, DIRS и APP_DIRS
Настройки шаблонизатора лежат в settings.py. В проекте после startproject они выглядят так (добавлена только строка DIRS):
# config/settings.py
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
DIRS— папки, общие для всего проекта. Сюда кладут базовый шаблонbase.html, шаблоны ошибок404.htmlи переопределённые шаблоны сторонних приложений.APP_DIRS: True— Django ищет папкуtemplatesвнутри каждого приложения изINSTALLED_APPS.context_processorsдобавляют переменные во все шаблоны, отрендеренные сrequest: например,userиmessages.
Порядок поиска: сначала DIRS, затем templates приложений в порядке INSTALLED_APPS. Используется первый найденный файл. Поэтому внутри templates приложения создают ещё одну папку с его именем:
mysite/
├── templates/ # DIRS: общие шаблоны
│ ├── base.html
│ └── includes/
│ └── nav.html
└── blog/
└── templates/
└── blog/ # пространство имён приложения
└── post_list.html
Тогда шаблон называется "blog/post_list.html" и не пересечётся с одноимённым шаблоном другого приложения. Подробнее о структуре приложения — в статье про startapp.
Переменные и точечная нотация
Всё, что передано в словаре контекста, доступно по имени. Точка работает универсально — Django по очереди пробует ключ словаря, атрибут или метод и индекс списка:
{{ post.title }} {# атрибут объекта #}
{{ data.key }} {# data["key"] #}
{{ items.0 }} {# items[0] #}
{{ post.get_absolute_url }} {# метод вызывается без скобок #}
Аргументы методам передать нельзя — шаблон намеренно ограничен, логика должна жить во view или модели. Несуществующая переменная не вызывает ошибку и выводится пустой строкой: {{ a.b.c }} при отсутствии a даст пустое место. Из-за этого опечатки в именах легко пропустить.
Теги: if, for, url, static
Условия: if, elif, else
{% if user.is_authenticated and posts|length > 2 %}
<p>Привет, {{ user.username }}!</p>
{% elif not posts %}
<p>Постов пока нет.</p>
{% else %}
<p>Войдите, чтобы читать всё.</p>
{% endif %}
Доступны and, or, not, сравнения ==, !=, <, >, <=, >=, а также in и not in. Скобки для группировки условий не поддерживаются.
Циклы: for и empty
{% for post in posts %}
<article class="{% cycle 'odd' 'even' %}">
<h2>{{ forloop.counter }}. {{ post.title }}</h2>
</article>
{% empty %}
<p>Постов пока нет.</p>
{% endfor %}
Блок {% empty %} выполняется, если список пуст. Внутри цикла доступна переменная forloop:
| Переменная | Значение |
|---|---|
forloop.counter | Номер итерации с 1 |
forloop.counter0 | Номер итерации с 0 |
forloop.first, forloop.last | True на первой / последней итерации |
forloop.parentloop | forloop внешнего цикла |
По словарю идут так: {% for key, value in data.items %}.
Ссылки: url
Не пишите адреса руками — тег url строит их по имени маршрута из urls.py:
<a href="{% url 'post_list' %}">Все посты</a>
<a href="{% url 'post_detail' post.slug %}">{{ post.title }}</a>
{# с app_name в urls.py приложения #}
<a href="{% url 'blog:post_detail' slug=post.slug %}">...</a>
Если поменять путь в urls.py, ссылки в шаблонах обновятся сами. Неверное имя даст ошибку NoReverseMatch.
Статика: static
CSS, JS и картинки подключаются тегом static, который сначала нужно загрузить:
{% load static %}
<link rel="stylesheet" href="{% static 'css/site.css' %}" />
<img src="{% static 'img/logo.png' %}" alt="Логотип" />
Тег склеивает путь со STATIC_URL: {% static 'css/site.css' %} превратится в /static/css/site.css. Как статика раздаётся в продакшне, рассказано в статье про деплой Django.
Комментарии и другие теги
{# однострочный комментарий — не попадёт в HTML #}
{% comment "почему скрыто" %}
<p>Многострочный блок</p>
{% endcomment %}
{% with total=posts|length %}Всего: {{ total }}{% endwith %}
{% firstof post.subtitle post.title "Без названия" %}
{% now "Y" %}
Фильтры
Фильтр пишется через |, аргумент — через двоеточие. Фильтры можно выстраивать в цепочку: {{ post.body|striptags|truncatewords:30 }}.
| Фильтр | Пример | Результат |
|---|---|---|
default | {{ missing|default:"нет" }} | нет, если значение ложное |
length | {{ posts|length }} | Количество элементов |
upper, lower, title | {{ "django"|upper }} | DJANGO |
truncatewords | {{ body|truncatewords:5 }} | Первые 5 слов и … |
truncatechars | {{ "Hello World"|truncatechars:6 }} | Hello… |
date | {{ created|date:"d.m.Y" }} | 09.10.2026 |
timesince | {{ created|timesince }} | 3 дня |
floatformat | {{ 3.14159|floatformat:2 }} | 3,14 при русской локали |
join | {{ tags|join:", " }} | a, b |
linebreaksbr | {{ text|linebreaksbr }} | Переносы строк → <br> |
striptags | {{ html|striptags }} | Текст без HTML-тегов |
slugify | {{ "Hello World"|slugify }} | hello-world |
safe | {{ html|safe }} | Вывод без экранирования |
Формат даты зависит от языка. При LANGUAGE_CODE = "ru" фильтр {{ created|date:"j E Y" }} выведет 9 октября 2026 — символ E даёт название месяца в родительном падеже, а F — в именительном («Октябрь»).
Важно: фильтр
pluralizeрассчитан на английский язык с двумя формами слова. Три русские формы («комментарий», «комментария», «комментариев») он не поддерживает — склоняйте число в Python-коде или своим фильтром.
Наследование шаблонов: extends и block
Чтобы не копировать шапку и подвал в каждую страницу, создайте базовый шаблон с «дырками» — блоками:
<!-- templates/base.html -->
{% load static %}
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>{% block title %}Мой блог{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/site.css' %}" />
</head>
<body>
{% include "includes/nav.html" %}
<main>{% block content %}{% endblock %}</main>
</body>
</html>
Дочерний шаблон наследует базовый и заполняет нужные блоки:
<!-- blog/templates/blog/post_list.html -->
{% extends "base.html" %}
{% block title %}Посты — {{ block.super }}{% endblock %}
{% block content %}
<h1>Посты ({{ posts|length }})</h1>
{% for post in posts %}
<h2><a href="{% url 'post_detail' post.slug %}">{{ post.title }}</a></h2>
<p>{{ post.body|truncatewords:30 }}</p>
{% endfor %}
{% endblock %}
Результат: <title>Посты — Мой блог</title> и список постов внутри <main>. Правила наследования:
{% extends %}должен быть первым тегом в шаблоне;- текст вне блоков в дочернем шаблоне игнорируется;
{{ block.super }}вставляет содержимое блока из родителя;- уровней может быть больше двух:
base.html→blog/base.html→blog/post_list.html; {% load %}не наследуется: если дочерний шаблон используетstatic, пропишите{% load static %}и в нём.
Подключение фрагментов: include
include вставляет другой шаблон целиком — удобно для меню, карточек, пагинации:
{% include "includes/nav.html" %}
{% include "includes/card.html" with title="Подписка" only %}
Подключённый шаблон видит весь текущий контекст. Через with передаются дополнительные переменные, а only ограничивает контекст только ими.
В Django 6.0 появились партиалы — именованные фрагменты внутри одного шаблона. Фрагмент объявляют тегом {% partialdef имя %}...{% endpartialdef %} и выводят тегом {% partial имя %}, а из view или include к нему обращаются по имени "шаблон.html#имя":
{% partialdef card %}
<article>{{ post.title }}</article>
{% endpartialdef %}
{% for post in posts %}{% partial card %}{% endfor %}
Свои фильтры и теги
Когда встроенных фильтров не хватает, создайте модуль в папке templatetags приложения. Рядом обязательно нужен пустой __init__.py, а приложение должно быть в INSTALLED_APPS:
# blog/templatetags/blog_extras.py
from django import template
register = template.Library()
@register.filter
def rub(value):
"""1234567 → «1 234 567 ₽»"""
return f"{value:,}".replace(",", " ") + " ₽"
@register.simple_tag
def reading_time(text, wpm=200):
words = len(text.split())
return max(1, round(words / wpm))
{% load blog_extras %}
{{ product.price|rub }} {# 1 234 567 ₽ #}
{% reading_time post.body %} мин {# 3 мин #}
После создания папки templatetags перезапустите runserver — новые библиотеки тегов подхватываются только при старте.
Автоэкранирование и safe
Django экранирует HTML во всех переменных: строка <script>alert(1)</script> выведется как <script>alert(1)</script> и отобразится текстом. Это защита от XSS — злоумышленник не сможет внедрить скрипт через комментарий или имя пользователя.
Отключать экранирование можно только для HTML, которому вы доверяете:
{{ post.html_body|safe }}
{% autoescape off %}
{{ trusted_html }}
{% endautoescape %}
В Python-коде то же делает django.utils.safestring.mark_safe(). Никогда не помечайте безопасным то, что ввёл пользователь, без очистки (например, библиотекой nh3).
Частые ошибки
TemplateDoesNotExist
Django не нашёл файл. Проверьте путь относительно папки templates ("blog/post_list.html", а не "templates/blog/post_list.html"), что приложение есть в INSTALLED_APPS при APP_DIRS: True и что общая папка указана в DIRS. Страница ошибки в режиме DEBUG перечисляет все проверенные пути — начните с неё.
Invalid block tag on line N: ‘static’
Полное сообщение заканчивается подсказкой Did you forget to register or load this tag?. В шаблоне нет {% load static %} — или он есть только в родительском шаблоне. Для своих тегов то же сообщение означает, что не загружена ваша библиотека.
Invalid filter: ‘rub’
Не загружен модуль с фильтром ({% load blog_extras %}), опечатка в имени или сервер не перезапущен после создания templatetags.
Unclosed tag on line N: ‘if’
Не хватает {% endif %}, {% endfor %} или {% endblock %}. Сообщение подсказывает строку и ожидаемые теги.
Переменная выводится пустой
Её нет в контексте, имя написано с опечаткой или view не передаёт словарь в render(). Переменные user и request появляются, только если шаблон отрендерен с request.
Частые вопросы
Как передать переменную во все шаблоны сразу?
Напишите свой context processor — функцию, которая принимает request и возвращает словарь, — и добавьте путь к ней в TEMPLATES["OPTIONS"]["context_processors"]:
# blog/context_processors.py
from django.conf import settings
def site(request):
return {"SITE_NAME": settings.SITE_NAME}
После этого {{ SITE_NAME }} доступна в каждом шаблоне, отрендеренном через render().
Как получить значение словаря по ключу из переменной?
Синтаксис {{ prices[code] }} в DTL не работает, а {{ prices.code }} ищет ключ "code" буквально. Нужен небольшой фильтр:
# blog/templatetags/dict_extras.py
from django import template
register = template.Library()
@register.filter
def get_item(mapping, key):
return mapping.get(key)
В шаблоне: {% load dict_extras %} и {{ prices|get_item:code }}.
Можно ли считать в шаблоне: складывать, умножать, делить?
Только в простых случаях: фильтр add складывает ({{ price|add:500 }}), а тег widthratio умножает и делит — {% widthratio done total 100 %} вернёт процент выполнения. Всё сложнее считайте во view или в методе модели и передавайте в шаблон готовое значение.
Как безопасно передать данные из шаблона в JavaScript?
Фильтром json_script: он выводит значение как JSON внутри тега script и экранирует символы, которыми можно было бы закрыть тег и внедрить код.
{{ chart|json_script:"chart-data" }}
<script>
const chart = JSON.parse(document.getElementById("chart-data").textContent);
</script>
Можно ли использовать Jinja2 вместо шаблонов Django?
Да, Django поддерживает Jinja2 как второй бэкенд в списке TEMPLATES (django.template.backends.jinja2.Jinja2). Синтаксис похож, но в Jinja2 методы вызываются со скобками, а фильтры — обычные Python-функции. Админка и многие сторонние приложения написаны на DTL, поэтому бэкенд DjangoTemplates в настройках оставляют.
Итог
Общие шаблоны храните в DIRS, шаблоны приложений — в app/templates/app/. Выводите данные через {{ }}, логику пишите тегами {% if %} и {% for %}, ссылки стройте через {% url %}, а общую разметку выносите в base.html с {% block %}. Как выводить в шаблоне формы, рассказано в статье «Формы в Django», а весь путь от установки до первого сайта собран в руководстве по Django для начинающих.
- #Django
- #Шаблоны
- #Django Templates
- #HTML
- #Фильтры