jinja2
Jinja is Python's default template engine. You write documents with {{ variable }} placeholders and {% for %}/{% if %} logic blocks, hand the template your data, and get rendered text back. Templates compile to Python bytecode just in time and get cached, so rendering is fast. It has template inheritance (a base layout with overridable blocks), macros that work like functions, dozens of built-in filters, autoescaping for HTML, a sandbox mode for untrusted templates, and async rendering. Flask uses it as its template layer, Ansible uses it for playbook expressions, and dbt uses it inside SQL, so its syntax has become the lingua franca of Python templating far beyond web pages.
The default answer for templating in Python, and the safe one: mature, fast, documented everywhere, with syntax half the industry already reads. Only skip it when a SPA makes server-side rendering pointless or when your templates are really programs that belong in Python.
Use it if
- You render server-side HTML in Flask, FastAPI, or Django-adjacent stacks and want inheritance, macros, and autoescaping instead of string formatting
- You generate any text artifact from data: config files, SQL, emails, Kubernetes manifests, code scaffolding; Jinja is the standard tool for all of these
- Your team already knows the syntax from Ansible, dbt, or Home Assistant, which makes templates readable by people who never touch your codebase
- You need to render templates from untrusted authors and want the sandboxed environment rather than rolling your own restrictions
- Your frontend is a React/Vue/Svelte SPA that gets JSON from the backend: server-side HTML templating is dead weight in that architecture
- You render user-supplied template strings without the sandbox: that is a server-side template injection hole leading straight to code execution, and even SandboxedEnvironment needs careful review before you bet on it
- You need heavy logic in templates: Jinja deliberately restricts Python in templates, and fighting that (or abusing filters as functions) produces unmaintainable template code that belongs in Python
- You want a fast-moving project: the 3.1 line has been current since 2022 with the last release in March 2025, which is stability, but do not expect new features quickly; it is a volunteer-run Pallets project
Setup reality
pip install jinja2 brings one dependency, MarkupSafe, and there is nothing to compile. The sharp edge is configuration: a bare Environment() has autoescaping OFF, so anyone rendering HTML must remember select_autoescape or ship an XSS bug; Flask turns it on for you, raw Jinja does not. Undefined variables silently render as empty strings by default, which hides typos until production; you almost always want undefined=StrictUndefined. Whitespace control (trim_blocks, lstrip_blocks, the minus sign in tags) is fiddly and a common source of mangled YAML or SQL output. Template paths resolve through loaders, so 'template not found' errors usually mean a wrong searchpath, not a missing file.
Patterns
Set up an Environment with autoescapingenvironment-setup
from jinja2 import Environment, FileSystemLoader, select_autoescape
env = Environment(
loader=FileSystemLoader('templates'),
autoescape=select_autoescape()
)
template = env.get_template('index.html')
print(template.render(title='Home', users=['ada', 'linus']))A bare Environment() does NOT autoescape; without select_autoescape any HTML rendering is an XSS bug waiting. Build one Environment per app, not per request, or you lose the compiled-template cache.
Render a one-off template stringrender-string-template
from jinja2 import Template
t = Template('Hello {{ name }}! You have {{ count }} new messages.')
print(t.render(name='Sam', count=3))Fine for quick text generation, but Template() creates a throwaway Environment each time and skips autoescaping; use a shared Environment for anything repeated or HTML.
Inherit from a base layouttemplate-inheritance
{# base.html #}
<html><head><title>{% block title %}{% endblock %}</title></head>
<body>{% block content %}{% endblock %}</body></html>
{# page.html #}
{% extends 'base.html' %}
{% block title %}Members{% endblock %}
{% block content %}
<ul>{% for user in users %}<li>{{ user.name }}</li>{% endfor %}</ul>
{% endblock %}extends must be the first tag in the child template; use {{ super() }} inside a block to keep the parent block's content instead of replacing it.
Register a custom filtercustom-filter
def currency(value, symbol='$'):
return f'{symbol}{value:,.2f}'
env.filters['currency'] = currency
# in the template:
# {{ order.total | currency }}
# {{ order.total | currency('EUR ') }}Filters are plain functions taking the piped value first; register them before loading templates. env.globals and env.tests work the same way for functions and 'is' checks.
Fail loudly on undefined variablesstrict-undefined
from jinja2 import Environment, FileSystemLoader, StrictUndefined
env = Environment(
loader=FileSystemLoader('templates'),
undefined=StrictUndefined
)
# {{ mispelled_var }} now raises UndefinedError instead of
# silently rendering as an empty stringThe default Undefined hides typos until someone notices blank output in production; StrictUndefined is the right setting for config and SQL generation especially.
Define and import macrosmacros
{# forms.html #}
{% macro input(name, value='', type='text') -%}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{%- endmacro %}
{# page.html #}
{% from 'forms.html' import input %}
{{ input('username') }}
{{ input('password', type='password') }}Imported macros do not see the calling template's variables unless you import 'with context', a distinction that trips most first-time macro users.
Control whitespace in generated textwhitespace-control
env = Environment(
loader=FileSystemLoader('templates'),
trim_blocks=True,
lstrip_blocks=True
)
# or per-tag with minus signs:
# {%- for host in hosts %}
# {{ host }}
# {%- endfor %}Essential when generating YAML, INI, or SQL where stray blank lines and indentation break the output; trim_blocks removes the newline after a block tag, lstrip_blocks the indentation before it.
Render templates from untrusted authorssandbox-untrusted-templates
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
t = env.from_string(user_supplied_template)
print(t.render(name='customer'))
# attribute probes like {{ ''.__class__.__mro__ }} raise SecurityErrorNever render user-supplied template strings in a normal Environment; that is server-side template injection. The sandbox blocks unsafe attribute access but still deserves a resource limit and review.
Render with async data sourcesasync-rendering
env = Environment(
loader=FileSystemLoader('templates'),
enable_async=True
)
async def handler():
template = env.get_template('report.html')
return await template.render_async(rows=fetch_rows())With enable_async=True templates can await coroutines and iterate async generators in for loops; render() stops working on that Environment, you must call render_async.
Load templates from packages with overridespackage-and-choice-loaders
from jinja2 import ChoiceLoader, FileSystemLoader, PackageLoader
env = Environment(loader=ChoiceLoader([
FileSystemLoader('overrides'), # checked first
PackageLoader('myapp', 'templates') # bundled defaults
]))ChoiceLoader tries loaders in order, which is the standard pattern for letting users override your library's built-in templates.
Include partials that may be missinginclude-with-defaults
{% include 'sidebar.html' ignore missing %}
{% include ['theme/header.html', 'default/header.html'] %}
{% for item in items %}
{% include 'item.html' %}
{% endfor %}A list of names uses the first template that exists; ignore missing silently skips absent partials instead of raising TemplateNotFound.
Generate config files, not HTMLgenerate-non-html-text
from jinja2 import Environment, FileSystemLoader, StrictUndefined
env = Environment(
loader=FileSystemLoader('templates'),
undefined=StrictUndefined,
trim_blocks=True,
lstrip_blocks=True,
keep_trailing_newline=True
)
conf = env.get_template('nginx.conf.j2').render(servers=servers)For text output leave autoescape off (it would insert HTML entities into your config), turn whitespace controls on, and keep_trailing_newline preserves POSIX-friendly file endings.