2025, Nov 07 01:00
Jinja2 template loops that output nothing: how to pass the right context and avoid write-mode truncation
Learn why your Jinja2 template loop renders nothing and how to fix context mismatches and write-mode truncation. Pick the right render pattern to include posts.
Rendering a list of posts with Jinja looks straightforward until the first template quietly outputs nothing. The typical culprits are a context mismatch between Python and the template, and writing to a file in a way that discards previous loop iterations. Below is a compact walkthrough of what goes wrong and how to structure the render step correctly so each post appears exactly once.
What the broken setup looks like
Consider a Jinja template that expects a list and iterates over it. The template loops over a collection, but inside the loop it displays scalar variables like title or body instead of using fields from the current item. Meanwhile, the Python code renders the template once per entry, passes a single item under the wrong key, and writes the result using write mode during every iteration.
{% 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}")
This setup leads to two practical problems. First, the template loops over records, but the render call provides a single entry and no records list, so the template has nothing to iterate and produces no article markup. Second, using write mode inside the loop truncates the file on each pass, so even if the template did output something per iteration, only the last iteration would survive on disk.
Why this happens
Jinja templates receive a context dictionary. If the template expects records, then the render call must provide a key named records that actually contains a list. Passing entry instead of records means the {% for rec in records %} loop never runs. That is why the HTML shows no articles.
The other issue is file I/O semantics. Opening a file in write mode on each iteration overwrites the previous content. Even a correct render loop will silently replace earlier posts unless you either append to the file or open the file once outside the loop and write sequentially.
There is also a common NameError side effect while experimenting: deleting a variable definition like headline but still referencing it in render(...) or in the template will raise NameError. If a variable is removed from Python, either stop passing it to render(...) and stop using it in the template, or replace it with rec-specific fields such as {{ rec.title }}.
Two correct ways to render
The first path renders the template once per post, removes the loop from the template, and writes in append mode or keeps the file open across iterations. The second path renders once for the entire collection and keeps the loop in the template. Both are valid; choose one and keep Python and Jinja in agreement.
Option A: Loop in Python, no loop in the template
In this approach, the template outputs a single article. Python iterates entries and appends each rendered article to the same file.
<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}")
If you prefer write mode, open the file once before the loop and keep it open until all entries are written.
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}")
Option B: Loop in the template, render once in Python
In this approach, Python provides the entire list under the key the template expects, and the template performs the loop. This keeps the output atomic and avoids file truncation issues during iteration.
{% 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")},
# more records here
]
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}")
This structure also eliminates the NameError class of issues because the template reads data from rec itself, not from unrelated top-level variables that may or may not exist in the render context.
Why this detail matters
Template rendering is contract-based. The template declares what keys it needs, and Python must supply those keys exactly once, under the same names. A one-letter mismatch between records and entry, or leaving a removed variable referenced, will result in empty output or runtime exceptions that cost time to debug. File write mode is a separate but equally important detail; replacing content in a loop creates a false impression that rendering runs while silently discarding earlier iterations.
Takeaways
Decide whether the loop lives in Python or in Jinja and stick to that decision consistently. If the loop is in the template, provide a list under the same key the template expects and read values via rec.field names. If the loop is in Python, remove the template loop and append rendered fragments or keep the file open while writing. Avoid passing conflicting or unused scalars into the context, and when removing a variable from Python, remove its usage in the template too. With those small adjustments, Jinja templates render predictably and your generated pages will contain every article you intended to publish.