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

bash
pip install jinja2

Output: (none — exits 0 on success)

bash
uv add jinja2

Output: dependency resolved + added to pyproject.toml

bash
poetry add jinja2

Output: updated lockfile + virtualenv install

bash
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.x line — 3.0 released in 2021, current is the 3.1.x series.
  • Python 3.7+ on older 3.1.x; 3.1.4+ drops 3.6 and below.
  • The 3.x series prefers explicit autoescape over the implicit guessing of 2.x; this is a deliberate security move.
  • 2.x is 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] — installs Babel for the gettext extension ({% trans %} blocks).
  • Jinja2[async] — no extra wheel install; enables async-aware filters and render_async(). Useful with FastAPI / Starlette.

Required transitively:

  • MarkupSafe — implements the Markup class and escape() function. Cython-accelerated; the C extension is optional but recommended.

Alternatives

PackageTrade-off
makoInline Python, faster than Jinja for compute-heavy templates, smaller ecosystem. Use when the template needs real Python expressions inline.
chameleonXML/ZPT-based; structurally typed templates. Used by Pyramid; niche outside that.
django.templateBundled 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-stringsBuilt-in. Fine for short interpolations; no separation between logic and template.
tenjin, cheetah3Largely 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).

python
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.

python
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 &lt;Dev&gt; — 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.

python
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 %}.

html
{# base.html #}
<!doctype html>
<title>{% block title %}Default{% endblock %}</title>
<main>{% block content %}{% endblock %}</main>
html
{# page.html #}
{% extends "base.html" %}
{% block title %}My page{% endblock %}
{% block content %}<p>Hello {{ name }}</p>{% endblock %}

Output (of page.html.render(name="Alice")):

html
<!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.

python
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
html
{# in the template, opt out per-variable #}
<p>{{ bio | safe }}</p>      {# DANGEROUS — renders literal <i> #}

Output: <p>&lt;i&gt;Hi&lt;/i&gt;</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.

python
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().

python
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 Environment per process. Building it scans loaders and parses the autoescape config. Once per process.
  • Use a real loader=FileSystemLoader(...), not from_string in a hot path. FileSystemLoader caches compiled templates; from_string parses every call.
  • Set cache_size=400 or higher on Environment if you have many templates. Default is 400; for thousands of micro-templates, raise it.
  • Use auto_reload=False in production. Default True checks file mtime on every render; flip to False once 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. Verify markupsafe._speedups imports 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 default autoescape=False was kept (compatibility), but select_autoescape is 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.4 minimum.
  • 3.1.x → 3.1.6+ — sandboxed environment improvements; XSS-vector fixes around xmlattr filter. Keep patches current.
python
# 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.6 minimum. The 3.1.x line has had multiple sandbox/security fixes; keep patches current.
  • auto_reload=False in 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 .cache files 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"]) or autoescape=True for any environment that renders HTML.
  • | safe defeats autoescape. Use only on strings you sanitized yourself (e.g. Markdown rendered through bleach).
  • SandboxedEnvironment is required for untrusted templates. Plain Environment exposes 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.x series 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 via PrefixLoader or ChoiceLoader.
  • xmlattr filter quoting — a sequence of fixes in 2024-2025 addressed attribute-name escaping. Don't pass user-controlled keys into xmlattr.

Testing & CI integration

python
# 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 '&lt;script&gt;' 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.

python
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_template wraps it.
  • Django — pluggable; install django-jinja for 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.
  • FastAPIfastapi.templating.Jinja2Templates wraps 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 / SymptomLikely causeFix
TemplateNotFound: page.htmlLoader path wrongVerify FileSystemLoader("templates") resolves to the right dir; env.list_templates() prints what's discoverable.
UndefinedError: 'x' is undefinedVariable not passedPass it in render(...) or set undefined=ChainableUndefined for chained-attribute tolerance.
Output is HTML-escaped when you wanted rawAutoescape on + missing `safe`
<script> rendered raw despite "autoescape"Autoescape was off, or you used `safe`
SecurityError in a sandboxed envUser template touched a banned attributeExpected; tighten the sandbox or reject the template.
RecursionError in templateInfinite {% include %} cycleAudit includes; the loader doesn't detect cycles.
TypeError: argument of type 'NoneType' is not iterableNone reached an iterationUse {% if items %}{% for ... %} or (items or []).
Encoded entities show as &amp;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

PythonJinja2 lineNotes
3.63.0, 3.1 earlyDrop floor (older 3.1.x).
3.73.1.xSupported on older patches.
3.8+3.1.x (current)Fully supported.
3.133.1.4+Free-threaded build works.

Pair compatibility:

  • MarkupSafe>=2.1 for Jinja2>=3.1.
  • Babel>=2.7 for the [i18n] extra.

See also