Синтаксис Jinja: переменные, циклы, условия, фильтры

Jinja — это маленький язык внутри HTML: переменные, циклы, условия и фильтры. Их хватает, чтобы собрать любую страницу из данных.
Логика шаблона ограничена намеренно: можно перебрать список, проверить условие, применить фильтр — но не написать сложный алгоритм. Это дисциплина: тяжёлые вычисления остаются в Python, шаблон только отображает.

Разберём ядро синтаксиса. Переменные выводятся через {{ }}, причём можно обращаться к атрибутам и элементам: {{ user.name }}, {{ items[0] }}, {{ data['key'] }}. Циклы и условия — через {% %}.

{% if user %}
  <p>Привет, {{ user.name }}!</p>
{% else %}
  <p>Войдите в систему</p>
{% endif %}

<ul>
{% for post in posts %}
  <li>{{ loop.index }}. {{ post.title }}</li>
{% else %}
  <li>Постов пока нет</li>
{% endfor %}
</ul>

Внутри for доступна переменная loop: loop.index (номер с 1), loop.first, loop.last. Блок {% else %} в цикле срабатывает, если список пуст — удобно для «нет данных».

Фильтры — функции через вертикальную черту: {{ name|upper }}, {{ price|round(2) }}, {{ text|truncate(100) }}, {{ items|length }}, {{ value|default('—') }}. Фильтры можно цепочкой: {{ name|trim|capitalize }}.

Ограниченность логики в шаблонах — это фича, а не недостаток. Jinja намеренно не даёт писать сложные алгоритмы, чтобы тяжёлые вычисления оставались в Python, где их легко тестировать и переиспользовать. Если внутри {% %} начинает зарождаться алгоритм с вложенными условиями — это сигнал вынести его во view или в кастомный фильтр. Фильтры, кстати, — самый элегантный способ повторно применять преобразования: дату форматируешь одним фильтром во всех шаблонах, и логика форматирования живёт в одном месте. Переменная loop с её index, first, last и length закрывает почти все потребности в нумерации и в особой отрисовке первого/последнего элемента без ручных счётчиков и подсчётов длины.

Как работает под капотом

Фильтр — это просто функция, которой значение слева передаётся первым аргументом. name|upper эквивалентно upper(name), а price|round(2)round(price, 2). Цепочка фильтров — это вложенные вызовы слева направо.

  {{ name | trim | capitalize }}
        │      │        │
        ▼      ▼        ▼
   "  bob "  "bob"    "Bob"
   значение  trim()  capitalize()
   (вход)   фильтр1   фильтр2

Смоделируем конвейер фильтров обычным Python — увидишь, что это просто последовательное применение функций.

FILTERS = {
    "upper": str.upper,
    "capitalize": str.capitalize,
    "trim": str.strip,
    "length": len,
}

def apply_filters(value, *filter_names):
    for name in filter_names:
        value = FILTERS[name](value)
    return value

print(apply_filters("  bob  ", "trim", "capitalize"))   # "Bob"
print(apply_filters("flask", "upper"))                    # "FLASK"
print(apply_filters([1, 2, 3], "length"))                 # 3

Запусти: значение проходит сквозь цепочку функций слева направо. В Jinja десятки встроенных фильтров и можно регистрировать свои через декоратор template_filter.

Частые ошибки

  • Забыть endfor/endif. Jinja требует явного закрытия блоков, иначе ошибка синтаксиса.
  • Сложная логика в шаблоне. Если внутри {% %} начинается алгоритм — вынеси его во view.
  • Путать фильтр и функцию. Фильтр — через |, значение идёт первым аргументом.

Best practices

  • Используй loop.index, loop.first/last вместо ручных счётчиков.
  • Применяй фильтр default для отсутствующих значений вместо if-обёрток.
  • Свои частые преобразования оформляй кастомными фильтрами, а не копипастой.

Что запомнить

  • Ядро Jinja: переменные, циклы for, условия if, фильтры через |.
  • Фильтр — функция, куда значение слева идёт первым аргументом; их можно цепочкой.
  • Переменная loop даёт index, first, last; {% else %} в for — для пустого списка.
  • Тяжёлые вычисления остаются в Python, шаблон только отображает.

Итог: Jinja даёт переменные, циклы с loop, условия и фильтры через |. Логика намеренно ограничена — тяжёлое остаётся в Python. Дальше — наследование шаблонов, чтобы не дублировать разметку.

Проверьте себя
1. Чему эквивалентен фильтр {{ price|round(2) }}?
Aprice + 2
Bround(price, 2) — значение слева идёт первым аргументом
C2 округлений price
Dprice в степени 2
2. Когда сработает блок {% else %} внутри цикла {% for %}?
AНа последней итерации
BЕсли перебираемый список пуст
CНикогда
DНа первой итерации