cheat sheet
jinja2
Package-level reference for Jinja2 on PyPI — install, version policy, autoescape gotchas, sandboxing, and the template features behind Flask, Ansible, and Sphinx.
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