Django-Admin.ru

Шаблоны Django: синтаксис, наследование и фильтры

Шаблоны Django с примерами: настройка TEMPLATES, переменные, теги if, for, url и static, фильтры, наследование через extends и block, include и свои фильтры.

Редакция Django-Admin.ru
· обновлено · 11 мин чтения

Содержание · 11 разделов
  1. Где Django ищет шаблоны: TEMPLATES, DIRS и APP_DIRS
  2. Переменные и точечная нотация
  3. Теги: if, for, url, static
  4. Условия: if, elif, else
  5. Циклы: for и empty
  6. Ссылки: url
  7. Статика: static
  8. Комментарии и другие теги
  9. Фильтры
  10. Наследование шаблонов: extends и block
  11. Подключение фрагментов: include
  12. Свои фильтры и теги
  13. Автоэкранирование и safe
  14. Частые ошибки
  15. TemplateDoesNotExist
  16. Invalid block tag on line N: ‘static’
  17. Invalid filter: ‘rub’
  18. Unclosed tag on line N: ‘if’
  19. Переменная выводится пустой
  20. Частые вопросы
  21. Как передать переменную во все шаблоны сразу?
  22. Как получить значение словаря по ключу из переменной?
  23. Можно ли считать в шаблоне: складывать, умножать, делить?
  24. Как безопасно передать данные из шаблона в JavaScript?
  25. Можно ли использовать Jinja2 вместо шаблонов Django?
  26. Итог

Шаблоны 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.lastTrue на первой / последней итерации
forloop.parentloopforloop внешнего цикла

По словарю идут так: {% 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> выведется как &lt;script&gt;alert(1)&lt;/script&gt; и отобразится текстом. Это защита от 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
  • #Фильтры
↑ ↓ выборEnter открытьEsc закрыть