skip to content

jinja2 — Python Template Engine

Package-level reference for Jinja2 on PyPI — install, version policy, autoescape gotchas, sandboxing, and the template features behind Flask, Ansible, and Sphinx.

10 min read 17 snippets deep dive

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

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

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

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

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

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

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#