2025, Nov 09 12:03

Два подхода к рендерингу списков в Jinja и Python

Разбираем, почему Jinja рендерит пусто и как исправить: согласовать контекст Python и шаблона, выбрать место для цикла и избежать перезаписи файла и NameError.

Рендеринг списка публикаций в Jinja кажется простым, пока первый шаблон внезапно не выдает пустой результат. Обычно виноваты несоответствие контекста между Python и шаблоном и запись в файл таким образом, что предыдущие итерации цикла затираются. Ниже — краткое объяснение, что идет не так, и как правильно организовать этап рендеринга, чтобы каждая запись появилась ровно один раз.

Как выглядит некорректная схема

Представим шаблон Jinja, который ожидает список и проходит по нему в цикле. Шаблон перебирает коллекцию, но внутри цикла выводит скалярные переменные вроде title или body вместо полей текущего элемента. В это же время код на Python рендерит шаблон по одному разу на запись, передает одиночный элемент под неверным ключом и при каждой итерации записывает результат в режиме перезаписи файла.

{% for rec in records %}
    <article>
        <h2><a class="permalink" href="/">{{ title }}</a></h2>
        <p>{{ body }}</p>
        <time>{{ date }}</time>
        <p class="tag">Tags: <a class="tags" href="/tags/{{ tags }}">{{ tags }}</a></p>
        <hr class="solid">
    </article>
{% endfor %}
# generate_pages.py
from jinja2 import Environment, FileSystemLoader
from datetime import datetime, timezone

headline = "First Post"
permalink = "first-post"
pub_date = 2025-7-6
content_text = "Hello world!"
labels = "First Post", "Second Tag"
entries = [
    {"title": "First Post"},
]

tmpl_env = Environment(loader=FileSystemLoader("templates"))
tmpl = tmpl_env.get_template("main.html")
output_path = "out.html"

for entry in entries:
    page_html = tmpl.render(
        entry,
        title=headline,
        slug=permalink,
        date=pub_date,
        body=content_text,
        tags=labels
    )
    with open(output_path, mode="w", encoding="utf-8") as fh:
        fh.write(page_html)
        print(f"... wrote {output_path}")

Такая схема приводит к двум практическим проблемам. Во‑первых, шаблон проходит по records, а в render передается один entry и вовсе нет списка records — в итоге перебирать нечего и разметка статей не генерируется. Во‑вторых, открытие файла в режиме записи внутри цикла обрезает файл на каждом проходе, поэтому даже при корректном выводе на итерацию на диске останется только последний результат.

Почему так происходит

Шаблоны Jinja получают словарь контекста. Если шаблон ожидает records, вызов render обязан передать ключ records, в котором действительно лежит список. Если передать entry вместо records, цикл {% for rec in records %} не запустится. Поэтому в HTML не появляется ни одной статьи.

Вторая проблема — семантика работы с файлами. Открытие файла в режиме записи на каждой итерации перезаписывает предыдущий контент. Даже корректный цикл рендеринга будет незаметно заменять ранние записи, если не включить режим добавления или не открыть файл один раз вне цикла и не писать последовательно.

Частый побочный эффект при экспериментах — NameError: вы удаляете определение переменной вроде headline, но продолжаете ссылаться на нее в render(...) или в шаблоне. В этом случае возникает NameError. Если переменную убрали из Python‑кода, перестаньте передавать ее в render(...) и использовать в шаблоне либо замените на поля текущего элемента, например {{ rec.title }}.

Два корректных подхода к рендерингу

Первый путь — рендерить шаблон один раз на публикацию, убрать цикл из шаблона и писать в режиме добавления либо держать файл открытым на протяжении всех итераций. Второй путь — рендерить один раз всю коллекцию и оставить цикл в шаблоне. Оба варианта рабочие; важно выбрать один и согласовать между собой Python и Jinja.

Вариант A: цикл в Python, в шаблоне — без цикла

В этом подходе шаблон выводит одну статью. Python перебирает entries и добавляет каждую отрендеренную статью в один и тот же файл.

<article>
    <h2><a class="permalink" href="/">{{ title }}</a></h2>
    <p>{{ body }}</p>
    <time>{{ date }}</time>
    <p class="tag">Tags: <a class="tags" href="/tags/{{ tags }}">{{ tags }}</a></p>
    <hr class="solid">
</article>
from jinja2 import Environment, FileSystemLoader
from datetime import datetime, timezone

headline = "First Post"
permalink = "first-post"
pub_date = 2025-7-6
content_text = "Hello world!"
labels = ("First Post", "Second Tag")
entries = [
    {"title": "First Post"},
]

env = Environment(loader=FileSystemLoader("templates"))
article_tpl = env.get_template("article.html")
output_file = "out.html"

for entry in entries:
    rendered = article_tpl.render(
        entry=entry,
        title=headline,
        slug=permalink,
        date=pub_date,
        body=content_text,
        tags=labels
    )
    with open(output_file, mode="a", encoding="utf-8") as fh:
        fh.write(rendered)
        print(f"... wrote {output_file}")

Если предпочитаете режим записи, откройте файл один раз перед циклом и держите его открытым до завершения записи всех элементов.

from jinja2 import Environment, FileSystemLoader

entries = [
    {"title": "First Post"},
]

env = Environment(loader=FileSystemLoader("templates"))
article_tpl = env.get_template("article.html")
output_file = "out.html"

with open(output_file, mode="w", encoding="utf-8") as fh:
    for entry in entries:
        rendered = article_tpl.render(entry=entry)
        fh.write(rendered)

print(f"... wrote {output_file}")

Вариант B: цикл в шаблоне, один вызов рендеринга в Python

Здесь Python передает весь список под ключом, которого ожидает шаблон, а цикл выполняется внутри шаблона. Так вывод формируется атомарно, и проблемы с обрезанием файла по ходу итераций исчезают.

{% for rec in records %}
    <article>
        <h2><a class="permalink" href="/">{{ rec.title }}</a></h2>
        <p>{{ rec.body }}</p>
        <time>{{ rec.date }}</time>
        <p class="tag">Tags: <a class="tags" href="/tags/{{ rec.tags }}">{{ rec.tags }}</a></p>
        <hr class="solid">
    </article>
{% endfor %}
from jinja2 import Environment, FileSystemLoader

records = [
    {"title": "First Post", "body": "Hello", "date": 2025-7-6, "tags": ("First Post", "Second Tag")},
    # здесь могут быть другие записи
]

env = Environment(loader=FileSystemLoader("templates"))
page_tpl = env.get_template("main.html")
output_file = "out.html"

page_html = page_tpl.render(records=records)
with open(output_file, mode="w", encoding="utf-8") as fh:
    fh.write(page_html)
    print(f"... wrote {output_file}")

Такая структура также исключает ошибки класса NameError: шаблон берет данные из самого rec, а не из сторонних переменных верхнего уровня, которые могут отсутствовать в контексте рендера.

Почему эти детали важны

Рендеринг шаблонов — это соглашение. Шаблон объявляет, какие ключи ему нужны, а Python обязан передать их ровно под теми же именами. Достаточно одной буквы разницы между records и entry или оставленной в шаблоне ссылки на удаленную переменную — получите пустой вывод или исключение во время выполнения и потерянное время на отладку. Режим записи в файл — отдельная, но не менее важная деталь: перезапись содержимого в цикле создаёт ложное ощущение, что всё работает, одновременно незаметно удаляя предыдущие результаты.

Итоги

Определитесь, где будет цикл — в Python или в Jinja — и последовательно придерживайтесь этого. Если цикл в шаблоне, передавайте список под ключом, который он ожидает, и обращайтесь к значениям через rec.поле. Если цикл в Python, уберите цикл из шаблона и добавляйте отрендеренные фрагменты либо держите файл открытым на время записи. Не передавайте в контекст конфликтующие или неиспользуемые скаляры, а удаляя переменную из Python‑кода, убирайте и её использование в шаблоне. С этими небольшими правками шаблоны Jinja будут предсказуемо отрабатывать, а на сгенерированных страницах окажутся все запланированные публикации.

Статья основана на вопросе на StackOverflow от dr_colossus и ответе furas.