Кладовочка

О разном, чтобы было под рукой онлайн

Шпаргалка по Jinja2: шаблонизатор Python

Jinja2 — быстрый и выразительный шаблонизатор для Python. Используется в FastApi, Flask, Django, Ansible, SaltStack, генераторах статических сайтов. Отделяет HTML-вёрстку от логики приложения: шаблон получает данные и рендерит готовую страницу.

Быстрый старт

pip install jinja2
from jinja2 import Environment, FileSystemLoader, Template

# Способ 1: из файла (для веб-приложений)
env = Environment(loader=FileSystemLoader("templates"))
template = env.get_template("hello.html")
html = template.render(name="Мир", items=["яблоко", "банан"])

# Способ 2: из строки (для простых случаев)
template = Template("Привет, {{ name }}!")
html = template.render(name="Мир")           # "Привет, Мир!"

Структура проекта:

project/
  templates/
    base.html
    index.html
  main.py

Базовый синтаксис

Три типа разделителей: {{ }} — выражение (выводит значение), {% %} — инструкция (управление потоком), {# #} — комментарий (не попадает в вывод).

{# Это комментарий — не появится в HTML #}

<!-- Переменная -->
<p>Привет, {{ user.name }}!</p>

<!-- Выражение -->
<p>Через 5 лет вам будет {{ user.age + 5 }}</p>

<!-- Обращение к элементам -->
{{ user["name"] }}              <!-- как к словарю -->
{{ user.name }}                 <!-- как к атрибуту (одинаково) -->
{{ items[0] }}                  <!-- элемент списка -->

Переменные и фильтры

Фильтры модифицируют значение перед выводом. Применяются через |. Можно соединять цепочкой: value|filter1|filter2.

Встроенные фильтры

<!-- Строки -->
{{ name|upper }}                <!-- "АННА" -->
{{ name|lower }}                <!-- "анна" -->
{{ name|capitalize }}           <!-- "Анна" -->
{{ name|title }}                <!-- "Анна Мария" -->
{{ text|trim }}                 <!-- убрать пробелы по краям -->
{{ text|truncate(100) }}        <!-- обрезать до 100 символов + "..." -->
{{ "Hello"|replace("Hello", "Привет") }}  <!-- замена -->
{{ "hello"|wordcount }}         <!-- 1 — количество слов -->

<!-- Числа -->
{{ price|round(2) }}            <!-- 3.14 -->
{{ 42|abs }}                    <!-- абсолютное значение -->
{{ items|length }}              <!-- длина списка/строки -->
{{ users|count }}               <!-- то же самое -->
{{ 1000000|intcomma }}          <!-- "1,000,000" (требует jinja2-humanize-extension) -->

<!-- Даты (datetime объект) -->
{{ user.created_at|datetimeformat('%d.%m.%Y') }}

<!-- Значения по умолчанию -->
{{ bio|default("Не указано") }}         <!-- если bio — None/пусто/не определено -->
{{ bio|default("Не указано", true) }}   <!-- считать undefined и пустую строку -->

<!-- HTML -->
{{ html|safe }}                 <!-- НЕ экранировать HTML (осторожно!) -->
{{ "<b>text</b>"|e }}           <!-- экранировать → &lt;b&gt;text&lt;/b&gt; -->

<!-- Работа со списками -->
{{ items|first }}               <!-- первый элемент -->
{{ items|last }}                <!-- последний -->
{{ items|random }}              <!-- случайный -->
{{ items|join(", ") }}          <!-- "яблоко, банан" -->
{{ [1, 2, 2, 3]|unique|list }} <!-- [1, 2, 3] (list нужен для вывода) -->

<!-- Формат (как в Python) -->
{{ "Имя: %s, Возраст: %d"|format(name, age) }}

<!-- JSON -->
{{ data|tojson }}               <!-- сериализация в JSON -->
{{ data|tojson(indent=2) }}     <!-- с отступами -->

Цепочки фильтров

{{ text|trim|lower|truncate(50) }}
{{ user.name|capitalize|default("Гость") }}

Управляющие конструкции

if/elif/else — условия

{% if user.is_admin %}
    <a href="/admin">Панель управления</a>
{% elif user.is_authenticated %}
    <a href="/profile">{{ user.name }}</a>
{% else %}
    <a href="/login">Войти</a>
{% endif %}

<!-- Тесты (is) -->
{% if user is defined %}                <!-- переменная определена? -->
{% if user is none %}                   <!-- None -->
{% if name is string %}                 <!-- тип str -->
{% if items is iterable %}              <!-- итерируемый объект -->
{% if score is number %}                <!-- int или float -->
{% if value is even %}                  <!-- чётное -->
{% if value is odd %}                   <!-- нечётное -->
{% if text is upper %}                  <!-- верхний регистр -->
{% if text is lower %}                  <!-- нижний регистр -->

for — циклы

<!-- Простой цикл -->
<ul>
{% for item in items %}
    <li>{{ item }}</li>
{% endfor %}
</ul>

<!-- else — если список пуст -->
{% for post in posts %}
    <article>{{ post.title }}</article>
{% else %}
    <p>Постов пока нет</p>
{% endfor %}

<!-- loop — специальная переменная внутри цикла -->
{% for user in users %}
    <p>
        {{ loop.index }}.           <!-- номер с 1 -->
        {{ loop.index0 }}           <!-- номер с 0 -->
        {{ loop.revindex }}         <!-- с конца, с 1 -->
        {{ loop.first }}            <!-- True для первого -->
        {{ loop.last }}             <!-- True для последнего -->
        {{ loop.length }}           <!-- общая длина -->
        {% if not loop.last %} | {% endif %}
    </p>
{% endfor %}

<!-- Цикл по словарю -->
{% for key, value in user.items() %}
    <dt>{{ key }}</dt><dd>{{ value }}</dd>
{% endfor %}

Наследование (extends/block)

Главный механизм переиспользования вёрстки. Базовый шаблон задаёт скелет страницы, дочерние — заполняют блоки.

base.html — базовый шаблон

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}Мой сайт{% endblock %}</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <header>
        {% block header %}
            <nav>{% include "nav.html" %}</nav>
        {% endblock %}
    </header>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>
        {% block footer %}
            <p>© {{ current_year }}</p>
        {% endblock %}
    </footer>

    {% block scripts %}{% endblock %}
</body>
</html>

index.html — дочерний шаблон

{% extends "base.html" %}

{% block title %}Главная — Мой сайт{% endblock %}

{% block content %}
    <h1>Последние посты</h1>
    {% for post in posts %}
        <article>
            <h2><a href="/post/{{ post.id }}">{{ post.title }}</a></h2>
            <p>{{ post.body|truncate(150) }}</p>
        </article>
    {% endfor %}
{% endblock %}

super() — содержимое родительского блока

{% block header %}
    {{ super() }}           <!-- содержимое header из base.html -->
    <div class="banner">Специальное предложение!</div>
{% endblock %}

Include — вставка фрагментов

include вставляет содержимое другого шаблона в текущий. В отличие от extends, не меняет структуру — просто «вклеивает».

<!-- nav.html -->
<nav>
    <a href="/">Главная</a>
    <a href="/about">О нас</a>
    {% if user %}
        <a href="/profile">{{ user.name }}</a>
        <a href="/logout">Выйти</a>
    {% else %}
        <a href="/login">Войти</a>
    {% endif %}
</nav>

<!-- base.html -->
<header>
    {% include "nav.html" %}
    {# или с ignore missing: #}
    {% include "sidebar.html" ignore missing %}
</header>

Макросы — переиспользуемые компоненты

Макрос — как функция в Python, но для HTML. Принимает аргументы, возвращает HTML-строку.

Определение макроса

{% macro input(name, label="", value="", type="text", required=false) %}
    <div class="form-group">
        {% if label %}
            <label for="{{ name }}">{{ label }}</label>
        {% endif %}
        <input
            type="{{ type }}"
            id="{{ name }}"
            name="{{ name }}"
            value="{{ value|e }}"
            {% if required %}required{% endif %}
        >
    </div>
{% endmacro %}

<!-- Использование -->
{{ input("username", label="Имя пользователя", required=true) }}
{{ input("email", type="email", label="Email") }}
{{ input("bio", type="textarea") }}

Импорт макросов из другого файла

<!-- forms.html — файл с макросами -->
{% macro input(name, label="", type="text") %}...{% endmacro %}
{% macro button(text, type="submit") %}...{% endmacro %}

<!-- index.html — используем -->
{% from "forms.html" import input, button %}
{% import "forms.html" as forms %}

{{ input("username", label="Логин") }}
{{ input("password", type="password", label="Пароль") }}
{{ forms.button("Войти") }}

Caller — передача содержимого в макрос

{% macro card(title) %}
    <div class="card">
        <h3>{{ title }}</h3>
        <div class="card-body">{{ caller() }}</div>
    </div>
{% endmacro %}

{% call card("Заголовок карточки") %}
    <p>Это содержимое карточки</p>
    <a href="/more">Подробнее →</a>
{% endcall %}

Whitespace control — управление пробелами

По умолчанию Jinja2 сохраняет пробелы и переводы строк вокруг тегов. - убирает их.

<!-- Без контроля: много пустых строк в HTML -->
{% for item in items %}
    {{ item }}
{% endfor %}

<!-- С контролем: компактный вывод -->
{% for item in items -%}
    {{ item }}
{%- endfor %}

<!-- Применить к одной стороне -->
{{- item }}    <!-- убрать пробелы слева -->
{{ item -}}    <!-- убрать пробелы справа -->

Raw, filter и другие специальные блоки

{# raw — вывести как есть, без обработки #}
{% raw %}
    Здесь можно писать {{ переменные }} и {% инструкции %}
    без их обработки Jinja2. Полезно для документации.
{% endraw %}

{# filter — применить фильтр ко всему содержимому блока #}
{% filter upper %}
    Весь этот текст
    станет заглавными буквами.
{% endfilter %}

{# set — присвоить значение переменной в шаблоне #}
{% set page_title = "Главная страница" %}
{% set items = items|sort|unique %}
{% set ns = namespace(count=0) %}       {# для изменения в циклах #}
{% for item in items %}
    {% set ns.count = ns.count + 1 %}
{% endfor %}

Кастомные фильтры и глобальные переменные

Расширение Jinja2 из Python-кода — собственные фильтры и функции, доступные во всех шаблонах.

Кастомный фильтр

from jinja2 import Environment, FileSystemLoader
from datetime import datetime

env = Environment(loader=FileSystemLoader("templates"))

def datetime_format(value, fmt="%d.%m.%Y"):
    if isinstance(value, str):
        value = datetime.fromisoformat(value)
    return value.strftime(fmt)

def ru_plural(value, forms):
    """Склонение: {{ 5|ru_plural("пользователь,пользователя,пользователей") }}"""
    n = abs(int(value)) % 100
    if n >= 11 and n <= 19:
        return f"{value} {forms.split(',')[2]}"
    n %= 10
    if n == 1:
        return f"{value} {forms.split(',')[0]}"
    if 2 <= n <= 4:
        return f"{value} {forms.split(',')[1]}"
    return f"{value} {forms.split(',')[2]}"

env.filters["datetimeformat"] = datetime_format
env.filters["ru_plural"] = ru_plural
{{ post.created_at|datetimeformat('%d.%m.%Y в %H:%M') }}
{{ comments_count|ru_plural("комментарий,комментария,комментариев") }}

Глобальные переменные и функции

from datetime import datetime

env.globals["current_year"] = datetime.now().year
env.globals["site_name"] = "Мой блог"
env.globals["is_production"] = False
<footer>© {{ current_year }} {{ site_name }}</footer>
{% if not is_production %}
    <div class="debug">Режим разработки</div>
{% endif %}

Контекстный процессор (как в Flask)

# Для FastAPI/Flask — добавить переменные во все шаблоны
@app.context_processor
def inject_globals():
    return {
        "current_year": datetime.now().year,
        "site_name": "Мой блог",
        "current_user": get_current_user(),
    }

Безопасность: автоэкранирование

Jinja2 по умолчанию экранирует HTML (< → &lt;) для защиты от XSS-атак. Если переменная содержит доверенный HTML — используйте |safe.

{# Опасно — если user_input = "<script>alert('xss')</script>" #}
{{ user_input|safe }}                  <!-- исполнится как JS! -->

{# Безопасно — экранирование по умолчанию #}
{{ user_input }}                       <!-- выведет <script> как текст -->

{# safe только для ЗАВЕДОМО доверенного контента #}
{{ article.html_content|safe }}        <!-- контент от админа — ок -->
# Отключить автоэкранирование (не рекомендуется)
env = Environment(loader=FileSystemLoader("templates"), autoescape=False)

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

Ошибка Причина Решение
UndefinedError: 'x' is undefined Переменная не передана в контекст Передать в render() или использовать default()
TemplateNotFound: base.html Неправильный путь к шаблонам Проверить FileSystemLoader("templates")
HTML рендерится как текст Забыт ` safe` для доверенного HTML
XSS-уязвимость ` safe` на пользовательском вводе
Многострочный вывод с лишними пробелами Jinja2 сохраняет whitespace {%- ... -%} для удаления пробелов
loop вне цикла loop доступен только внутри {% for %} Использовать только внутри for

Ресурсы

Что дальше


Связанные шпаргалки: Python, FastAPI, Django, SQLAlchemy