jinja2#
What it is#
Jinja2 is the dominant template engine for Python. It compiles templates to Python bytecode at first use, supports inheritance ({% extends %}), macros (reusable template-side functions), filters ({{ value | upper }}), tests ({% if value is none %}), and a sandboxed environment for executing untrusted templates. It is the rendering engine behind Flask, Ansible, Salt, Sphinx (for some output formats), Jupyter (nbconvert), and a long list of static site generators.
Reach for Jinja2 whenever you need to interpolate values into structured text: HTML pages, YAML/JSON configs, emails, infrastructure manifests, code generators. The mental model is small — variables, filters, blocks, includes, inheritance — and the autoescape behavior makes HTML output safe by default if you turn autoescape on. That “if” is the #1 footgun and the reason Jinja2 has one of the longest “security considerations” sections in this catalog.
Install#
pip install jinja2
Output: (none — exits 0 on success)
uv add jinja2
Output: dependency resolved + added to pyproject.toml
poetry add jinja2
Output: updated lockfile + virtualenv install
pip install "jinja2[i18n]" # adds Babel for the gettext/i18n extension
pip install "jinja2[async]" # enables async filters/tests; nothing extra to install on 3.x
Output: Jinja2 with the named extras. MarkupSafe is the one always-required transitive dep — it implements the Markup type for safe HTML.
Versioning & Python support#
- Stable
3.xline —3.0released in 2021, current is the3.1.xseries. - Python 3.7+ on older
3.1.x;3.1.4+ drops 3.6 and below. - The
3.xseries prefers explicit autoescape over the implicit guessing of2.x; this is a deliberate security move. 2.xis end-of-life. Don’t start new projects on it.
Package metadata#
- Maintainer: Pallets project (the same org that maintains Flask, Click, Werkzeug)
- Project home: github.com/pallets/jinja
- Docs: jinja.palletsprojects.com
- PyPI: pypi.org/project/Jinja2
- License: BSD-3-Clause
- First released: 2008
- Downloads: several hundred million per month — Flask, Ansible, Sphinx, MkDocs, and most data-engineering DSLs depend on it
Optional dependencies & extras#
Jinja2[i18n]— installsBabelfor thegettextextension ({% trans %}blocks).Jinja2[async]— no extra wheel install; enablesasync-aware filters andrender_async(). Useful with FastAPI / Starlette.
Required transitively:
MarkupSafe— implements theMarkupclass andescape()function. Cython-accelerated; the C extension is optional but recommended.
Alternatives#
| Package | Trade-off |
|---|---|
mako | Inline Python, faster than Jinja for compute-heavy templates, smaller ecosystem. Use when the template needs real Python expressions inline. |
chameleon | XML/ZPT-based; structurally typed templates. Used by Pyramid; niche outside that. |
django.template | Bundled with Django; less powerful than Jinja2. Use only inside Django (and even then, swapping in Jinja2 is common). |
string.Template (stdlib) | One-line substitution. Fine for tiny strings; no logic, no loops. |
f-strings | Built-in. Fine for short interpolations; no separation between logic and template. |
tenjin, cheetah3 | Largely historical. Use Jinja2. |
Real-world recipes#
Jinja2’s API is small: Environment for configuration, Template for compiled templates, render()/render_async() for output. The recipes below cover the patterns you actually use in production.
Recipe 1 — Render a template from a string (one-shot scripts).
from jinja2 import Template
t = Template("Hello, {{ name }}!")
print(t.render(name="Alice Dev"))
Output: Hello, Alice Dev!
Bare Template(...) defaults to autoescape off — fine for plain text, dangerous for HTML. Use Environment for HTML output.
Recipe 2 — Render from a directory of templates with autoescape on.
from jinja2 import Environment, FileSystemLoader, select_autoescape
env = Environment(
loader=FileSystemLoader("templates"),
autoescape=select_autoescape(["html", "htm", "xml"]),
)
print(env.get_template("page.html").render(user={"name": "Alice <Dev>"}))
Output: Alice <Dev> — autoescape kicks in because the filename ended in .html. Without select_autoescape, the literal < would render unescaped and you’d have an XSS bug.
Recipe 3 — Custom filter.
from jinja2 import Environment
def thousands(value: int) -> str:
return f"{value:,}"
env = Environment()
env.filters["thousands"] = thousands
print(env.from_string("Revenue: ${{ amount | thousands }}").render(amount=12345678))
Output: Revenue: $12,345,678 — filters are just callables registered on the Environment.
Recipe 4 — Template inheritance with {% extends %} + {% block %}.
{# base.html #}
<!doctype html>
<title>{% block title %}Default{% endblock %}</title>
<main>{% block content %}{% endblock %}</main>
{# page.html #}
{% extends "base.html" %}
{% block title %}My page{% endblock %}
{% block content %}<p>Hello {{ name }}</p>{% endblock %}
Output (of page.html.render(name="Alice")):
<!doctype html>
<title>My page</title>
<main><p>Hello Alice</p></main>
The child template overrides only the blocks it defines; everything else flows from the base. This is the workhorse pattern for any multi-page site.
Recipe 5 — Autoescape gotcha and the | safe filter.
env = Environment(autoescape=True)
t = env.from_string("<p>{{ bio }}</p>")
print(t.render(bio="<i>Hi</i>")) # safe — escaped
print(t.render(bio="<i>Hi</i>" | env.filters['safe'])) # also escaped, filter is template-side
{# in the template, opt out per-variable #}
<p>{{ bio | safe }}</p> {# DANGEROUS — renders literal <i> #}
Output: <p><i>Hi</i></p> first, <p><i>Hi</i></p> second. The | safe filter marks a string as “already HTML-safe”; use it only on values you produced (Markdown-rendered HTML, sanitized snippets). Never pipe user input through | safe without a sanitizer.
Recipe 6 — Sandboxed environment for untrusted templates.
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
t = env.from_string("Hello {{ name }}; banned: {{ ().__class__ }}")
try:
print(t.render(name="Alice"))
except Exception as e:
print(f"blocked: {type(e).__name__}")
Output: blocked: SecurityError — the sandbox blocks attribute access onto Python internals. If you let users supply templates (CMS, multi-tenant SaaS), you MUST use SandboxedEnvironment or ImmutableSandboxedEnvironment — the regular Environment is sandbox-free and gives users full Python attribute traversal.
Recipe 7 — Async render with render_async().
import asyncio
from jinja2 import Environment
env = Environment(enable_async=True)
async def main():
t = env.from_string("Hi {{ name }}")
print(await t.render_async(name="Alice"))
asyncio.run(main())
Output: Hi Alice — async-mode unlocks {% for x in async_iter %} inside templates and works cleanly inside FastAPI handlers.
Performance tuning#
Jinja2 compiles templates to Python bytecode on first load and caches the compiled form in the Environment. After warm-up, rendering is mostly attribute access and string concatenation.
- Cache the
Environmentper process. Building it scans loaders and parses the autoescape config. Once per process. - Use a real
loader=FileSystemLoader(...), notfrom_stringin a hot path.FileSystemLoadercaches compiled templates;from_stringparses every call. - Set
cache_size=400or higher onEnvironmentif you have many templates. Default is 400; for thousands of micro-templates, raise it. - Use
auto_reload=Falsein production. DefaultTruechecks file mtime on every render; flip toFalseonce you don’t change templates at runtime. MarkupSafe’s C extension is significant. On CPython, the speedup is 5-10× over the pure-Python fallback. Verifymarkupsafe._speedupsimports successfully — a missing wheel falls back silently.- Prefer filters over Python-side preprocessing for simple transforms. Filters run in the compiled template; per-render Python prep adds an extra pass.
- Don’t loop in templates if the loop is large. Render small fragments per row in Python, then concatenate; large
{% for %}blocks compile fine but lose readability.
Version migration guide#
2.x → 3.0— dropped Python 2, removed deprecated APIs. The defaultautoescape=Falsewas kept (compatibility), butselect_autoescapeis the recommended way forward.3.0 → 3.1— fixed a small autoescape regression. Internal API changes only.3.1.2 → 3.1.3— security fix for sandbox bypass (CVE-2024-22195). Pin minimum>=3.1.3.3.1.3 → 3.1.4— additional sandbox hardening (CVE-2024-34064). Bump to>=3.1.4minimum.3.1.x → 3.1.6+— sandboxed environment improvements; XSS-vector fixes aroundxmlattrfilter. Keep patches current.
# Before (2.x defaults)
env = Environment(loader=FileSystemLoader("tmpl")) # autoescape OFF — XSS risk
# After (3.x recommended)
from jinja2 import select_autoescape
env = Environment(
loader=FileSystemLoader("tmpl"),
autoescape=select_autoescape(["html", "htm", "xml"]),
)
Output: the second form HTML-escapes by default in HTML files; the first does not.
Production deployment notes#
- Pin
Jinja2>=3.1.6minimum. The 3.1.x line has had multiple sandbox/security fixes; keep patches current. auto_reload=Falsein production. Mtime checks are wasted I/O on a deployed app.cache_size=matches your template count. For Flask/Sphinx, the default is fine. For a code-generator with 1,000 templates, bump it.- Bake compiled templates with
env.compile_templates(target_dir, zip=None)for cold-start gains in serverless. Compiled.cachefiles load faster than parsing source. - Don’t load templates from a user-writable directory. A writable templates dir = arbitrary code execution.
- Health check your renderer. A simple
env.from_string("ok").render()on startup catches missing MarkupSafe wheels early.
Security considerations#
Jinja2’s security story is mostly: autoescape and sandbox correctly, or you have a problem.
- Autoescape is OFF by default. This is the #1 production bug. Always use
select_autoescape(["html", "htm", "xml"])orautoescape=Truefor any environment that renders HTML. | safedefeats autoescape. Use only on strings you sanitized yourself (e.g. Markdown rendered throughbleach).SandboxedEnvironmentis required for untrusted templates. PlainEnvironmentexposes Python attribute traversal —{{ ().__class__.__base__.__subclasses__() }}returns every loaded class. Multi-tenant SaaS and CMS scenarios MUST sandbox.Environment.add_extension('jinja2.ext.do')and other extensions can expand attack surface. Default-off is safer for sandboxes.- CVEs: the
3.1.xseries has shipped several sandbox-escape fixes (CVE-2024-22195, CVE-2024-34064, etc.). Keep current. - String-format template injection. Never do
f"Hello {{ {user_input} }}"— that lets users inject template syntax. Always pass values as render variables, not by f-string into the template source. {% include %}reads from the loader. Make sure user-controlled names don’t point at sensitive files; constrain viaPrefixLoaderorChoiceLoader.xmlattrfilter quoting — a sequence of fixes in 2024-2025 addressed attribute-name escaping. Don’t pass user-controlled keys intoxmlattr.
Testing & CI integration#
# pip install pytest
from jinja2 import Environment, select_autoescape
def make_env():
return Environment(autoescape=select_autoescape(["html"]))
def test_autoescape_protects_against_xss():
env = make_env()
out = env.from_string('<p>{{ x }}</p>').render(x='<script>alert(1)</script>')
assert '<script>' not in out
assert '<script>' in out
def test_safe_filter_emits_raw_html():
env = make_env()
out = env.from_string('<p>{{ x | safe }}</p>').render(x='<b>ok</b>')
assert '<b>ok</b>' in out
Output: both tests pass. The autoescape test is a one-line safety net worth adding to any project that renders HTML.
def test_sandbox_blocks_python_attribute_traversal():
from jinja2.sandbox import SandboxedEnvironment
import pytest, jinja2
env = SandboxedEnvironment()
with pytest.raises(jinja2.exceptions.SecurityError):
env.from_string("{{ ().__class__ }}").render()
Output: test passes — the sandbox raises SecurityError for the classic attribute-traversal payload.
Ecosystem integrations#
- Flask — Jinja2 is the default templating engine;
render_templatewraps it. - Django — pluggable; install
django-jinjafor Jinja2 templates in Django views. - Ansible — every playbook variable substitution is Jinja2.
- Salt — same.
- MkDocs / Material for MkDocs — used internally for theme templates.
- Sphinx — uses Jinja2 for HTML theme rendering.
- Jupyter nbconvert — templates are Jinja2.
- FastAPI —
fastapi.templating.Jinja2Templateswraps it for SSR. - Cookiecutter — project-scaffolding templates are Jinja2 + variables.
- Hugo / Hexo / Eleventy — not Jinja-based, but the mental model transfers.
Troubleshooting common errors#
| Error / Symptom | Likely cause | Fix |
|---|---|---|
TemplateNotFound: page.html | Loader path wrong | Verify FileSystemLoader("templates") resolves to the right dir; env.list_templates() prints what’s discoverable. |
UndefinedError: 'x' is undefined | Variable not passed | Pass it in render(...) or set undefined=ChainableUndefined for chained-attribute tolerance. |
| Output is HTML-escaped when you wanted raw | Autoescape on + missing ` | safe` |
<script> rendered raw despite “autoescape” | Autoescape was off, or you used ` | safe` |
SecurityError in a sandboxed env | User template touched a banned attribute | Expected; tighten the sandbox or reject the template. |
RecursionError in template | Infinite {% include %} cycle | Audit includes; the loader doesn’t detect cycles. |
TypeError: argument of type 'NoneType' is not iterable | None reached an iteration | Use {% if items %}{% for ... %} or (items or []). |
Encoded entities show as &amp; | Double-escaping (escaped HTML re-escaped) | Use Markup(...) or ` |
When NOT to use this#
- One-line string interpolation.
f"Hello {name}"is faster, simpler, and safer (no autoescape question). - Heavy in-template logic. If you’re writing
{% if x %}{% for y %}{% set z %}more than 10 lines deep, push the logic into Python and pass a flat dict. - JavaScript / browser-side templating. Use Mustache, Handlebars, or a real component framework.
- Compute-heavy templates. Mako is faster for templates with significant in-line Python expressions.
- You need a typed DSL. Look at typed-template projects (TSX, Lit) instead.
Compatibility matrix#
| Python | Jinja2 line | Notes |
|---|---|---|
| 3.6 | 3.0, 3.1 early | Drop floor (older 3.1.x). |
| 3.7 | 3.1.x | Supported on older patches. |
| 3.8+ | 3.1.x (current) | Fully supported. |
| 3.13 | 3.1.4+ | Free-threaded build works. |
Pair compatibility:
MarkupSafe>=2.1forJinja2>=3.1.Babel>=2.7for the[i18n]extra.
See also#
- Python: Flask — Jinja2’s most visible application
- Packages: pip-flask — Flask as a package