Jinja — Templates for Python¶
Jinja (package Jinja2) is a text template engine: it takes a template with placeholders and logic, adds data from Python, and produces text. The output can be anything — HTML, Markdown, YAML, JSON, XML, SQL, Python code or an LLM prompt. Templates are compiled to Python code once and then rendered fast. HTML escaping comes from its companion library MarkupSafe.
flowchart LR
D[Data<br/>dict / objects] --> R[Template.render]
T[Template<br/>.j2 / .html / string] --> E[Environment<br/>loader, filters, undefined, autoescape]
E --> R
R --> O[Text<br/>HTML / YAML / prompt / code]
Where QA Engineers Meet Jinja¶
| Place | What Jinja does there |
|---|---|
| Test data and config generation | Render per-environment YAML/JSON configs, request bodies, XML/SOAP payloads |
| Test reports | HTML reports, Markdown summaries for CI (for example $GITHUB_STEP_SUMMARY), release notes |
| LLM prompts | Prompt templates in your own code, LangChain template_format="jinja2", Hugging Face chat templates (tokenizer.chat_template) |
| Ansible | Variables in playbooks, ansible.builtin.template module for .j2 files |
| dbt | SQL models with {{ ref('orders') }}, macros and control flow |
| Web apps under test | Flask render_template, FastAPI / Starlette Jinja2Templates |
| Docs sites | MkDocs and Material theme templates; Zensical renders the same theme templates with MiniJinja, a Rust engine for Jinja syntax (this site's overrides/ are such templates) |
| Project scaffolding | Cookiecutter and Copier render whole project trees from Jinja templates |
Helm charts look similar but use Go templates, not Jinja. The ideas (variables, loops, escaping, missing values) are the same.
Installation¶
uv add jinja2 # installs MarkupSafe as a dependency
uv run python -c "import importlib.metadata as m; print(m.version('jinja2'), m.version('markupsafe'))"
Examples in this guide were run with Jinja2 3.1.6 and MarkupSafe 3.0.3 on Python 3.13. Use importlib.metadata for the version — the __version__ attribute is deprecated in MarkupSafe and is being deprecated in Jinja.
Section Map¶
| File | Topics |
|---|---|
| 01 Syntax Basics | Variables, expressions, filters, tests, if / for, loop, whitespace control, comments, raw, set, namespace |
| 02 Environment, Loaders & Inheritance | Environment, loaders, extends / block / super(), include, macros, import, call, context, globals |
| 03 Filters, Escaping & Security | Custom filters and tests, autoescape, Markup, StrictUndefined, sandbox, SSTI, async, NativeEnvironment |
| 04 QA Recipes | Configs and payloads, HTML and Markdown reports from JUnit XML, LLM prompts, generated pytest modules, Jinja in YAML |
| 05 Testing Templates | pytest for templates, missing variables, golden files and snapshots, custom filters, linting, common pitfalls |
Minimal Example¶
from jinja2 import Environment, StrictUndefined
env = Environment(undefined=StrictUndefined, trim_blocks=True, lstrip_blocks=True)
template = env.from_string("""\
Run: {{ run_name }}
{% for t in tests %}
- {{ t.name }}: {{ t.status | upper }}
{% endfor %}
""")
print(template.render(
run_name="smoke",
tests=[{"name": "login", "status": "passed"}, {"name": "logout", "status": "failed"}],
))
# Run: smoke
# - login: PASSED
# - logout: FAILED
Delimiters¶
| Syntax | Meaning |
|---|---|
{{ ... }} |
Expression — the result goes into the output |
{% ... %} |
Statement — if, for, set, block, macro, include, … |
{# ... #} |
Comment — not in the output |
{%- ... -%}, {{- ... -}} |
Same, but strip whitespace before / after the tag |
Quick Rules¶
- Create one
Environmentper template set and reuse it — it caches compiled templates. - Use
StrictUndefinedfor configs, prompts, generated code and in tests — a missing variable must fail, not render as an empty string. - Turn on autoescape for HTML/XML (
select_autoescape(["html", "xml"])) and keep it off for plain text, YAML and prompts. - Never build template source from user input — pass user data as variables.
from_string(f"... {user_input}")is server-side template injection. - Use
SandboxedEnvironmentonly when templates themselves come from users, and keep Jinja updated — sandbox bypasses were fixed in 3.1.5 and 3.1.6. - Keep logic in Python — templates should format data, not calculate it. Pass ready values or add a custom filter.
- Use
trim_blocks=Trueandlstrip_blocks=Truefor text formats where whitespace matters (YAML, Markdown, code). - Test templates like code — render with fixed data in pytest, compare with golden files, lint HTML templates.