Jinja — Syntax Basics¶
All examples render with a default Environment() unless the text says otherwise:
from jinja2 import Environment
env = Environment()
env.from_string("{{ user.name }}").render(user={"name": "Alice"}) # 'Alice'
Variables & Expressions¶
{{ user.name }} {# attribute, then item lookup #}
{{ user["name"] }} {# item, then attribute lookup #}
{{ items[0] }} {# index #}
{{ 2 ** 10 }} {{ 7 // 2 }} {{ 7 % 2 }} {# 1024 3 1 #}
{{ "run-" ~ build_id }} {# ~ converts both sides to strings and joins them #}
{{ "yes" if flag else "no" }}
{{ 3 in [1, 2, 3] }} {# True #}
{{ user.name | upper }} {# filter #}
user.name tries getattr(user, "name") first and falls back to user["name"]. With a dict, a key named like a dict method is shadowed by the method: {{ d.items }} prints <built-in method items ...>, {{ d["items"] }} returns the value. Use brackets for keys such as items, keys, values, get.
Literals: strings ("a", 'a'), numbers, lists [1, 2], tuples (1, 2), dicts {"a": 1}, true / false / none (lowercase; True / False / None also work).
Filters¶
A filter transforms a value: value | filter(args). Filters chain from left to right.
{{ name | default("anonymous") }} {# only when name is undefined #}
{{ "" | default("empty", true) }} {# true → also for falsy values #}
{{ users | map(attribute="name") | join(", ") }} {# a, b #}
{{ users | selectattr("active") | map(attribute="name") | list }}
{{ users | selectattr("age", "ge", 18) | list }} {# test with an argument #}
{{ codes | select("ge", 500) | list }} {# [500, 503] #}
{{ users | sort(attribute="age", reverse=true) | first }}
{{ results | sum(attribute="duration") | round(2) }}
{{ "%s-%03d" | format("case", 7) }} {# case-007 #}
{{ data | tojson }} {# JSON, with <, >, &, ' escaped as < etc. #}
| Group | Filters |
|---|---|
| Strings | upper, lower, title, capitalize, trim, replace, truncate, wordwrap, indent, center, striptags, urlencode |
| Numbers | int, float, round, abs, filesizeformat |
| Lists | length / count, first, last, join, sort, unique, reverse, min, max, sum, batch, slice, list |
| Select / map | map, select, reject, selectattr, rejectattr |
| Dicts | items, dictsort |
| Grouping | groupby — returns (grouper, list) pairs sorted by the key |
| Output | tojson, escape / e, safe, forceescape, xmlattr, pprint |
| Fallback | default / d |
{% for group in results | groupby("status") %}
{{ group.grouper }}: {{ group.list | length }}
{% endfor %}
{# failed: 1, passed: 2 — groups are sorted by the key #}
default does not replace none: {{ value | default("n/a") }} prints None when value=None. Use {{ value if value is not none else "n/a" }} or default("n/a", true) (which also replaces 0, "" and []).
Tests¶
A test checks a value and returns a boolean: value is test, value is not test.
{% if user is defined and user.email is not none %}...{% endif %}
{{ 4 is even }} {{ 9 is divisibleby 3 }} {{ 2 is in [1, 2] }}
{{ "a" is string }} {{ 1 is number }} {{ {} is mapping }} {{ [1] is iterable }}
Common tests: defined, undefined, none, boolean, true, false, string, number, integer, float, mapping, sequence, iterable, callable, even, odd, divisibleby, in, sameas, eq / ==, ne, lt, le, gt, ge, lower, upper. Tests also work in select / selectattr (selectattr("status", "equalto", "failed")).
Conditions¶
{% if code >= 500 %}server error{% elif code >= 400 %}client error{% else %}ok{% endif %}
Loops¶
{% for t in tests %}
{{ loop.index }}/{{ loop.length }} {{ t.name }}{% if loop.last %} (last){% endif %}
{% else %}
no tests collected
{% endfor %}
The else branch runs when the sequence is empty. Filter items inline — loop then counts only the kept items:
{% for t in tests if t.status != "skipped" %}{{ loop.index }}:{{ t.name }} {% endfor %}
{# 1:login 2:logout #}
loop attribute |
Value |
|---|---|
loop.index / loop.index0 |
Position from 1 / from 0 |
loop.revindex / loop.revindex0 |
Position from the end |
loop.first / loop.last |
First / last iteration |
loop.length |
Number of items |
loop.previtem / loop.nextitem |
Neighbour items (undefined at the edges) |
loop.cycle("odd", "even") |
Cycle through values — row striping |
loop.changed(value) |
True when the value differs from the previous iteration |
loop.depth / loop.depth0 |
Nesting level in recursive loops |
{% for key, value in env_vars | dictsort %}{{ key }}={{ value }}
{% endfor %}
{# Recursive loop over a tree of suites (trim_blocks + lstrip_blocks on) #}
{% for node in tree recursive %}
{{ " " * (loop.depth - 1) }}{{ node.name }}
{{ loop(node.children) }}
{%- endfor %}
{% break %} and {% continue %} need the extension: Environment(extensions=["jinja2.ext.loopcontrols"]). Without it they are a TemplateSyntaxError. Usually an inline if filter on the loop reads better.
Whitespace Control¶
By default, every tag line leaves its newline in the output:
src = """<ul>
{% for t in tests %}
<li>{{ t.name }}</li>
{% endfor %}
</ul>"""
Environment().from_string(src).render(tests=tests)
# '<ul>\n\n <li>login</li>\n\n <li>logout</li>\n\n</ul>'
Environment(trim_blocks=True, lstrip_blocks=True).from_string(src).render(tests=tests)
# '<ul>\n <li>login</li>\n <li>logout</li>\n</ul>'
| Option / syntax | Effect |
|---|---|
trim_blocks=True |
Remove the first newline after a block tag ({% %}) |
lstrip_blocks=True |
Strip spaces and tabs before a block tag at the start of a line |
{%- / -%} (also {{-, -}}) |
Strip all whitespace, including newlines, before / after this tag |
{%+ |
Turn lstrip_blocks off for this tag |
keep_trailing_newline=True |
Keep the final newline of the template (default: removed) |
For YAML, Markdown and generated code, set all three options on the environment; use - only for single tags.
Comments & Raw¶
{# This is not rendered. Also works across
several lines. #}
{% raw %}{{ this is printed as is }}{% endraw %}
raw is how you put literal {{ }} in the output — for example GitHub Actions ${{ secrets.TOKEN }} or an Ansible expression inside a Jinja-generated file.
Assignments & Scoping¶
{% set total = tests | length %}
{% set header %}Run #{{ build_id }}{% endset %} {# block set: captures rendered text #}
{% with failed = tests | selectattr("status", "equalto", "failed") | list %}
{{ failed | length }} failed
{% endwith %} {# failed exists only inside with #}
{% filter upper %}whole block in upper case{% endfilter %}
if does not create a scope, for does. A set inside a loop does not change the outer variable:
{% set found = false %}
{% for t in tests %}{% if t.status == "failed" %}{% set found = true %}{% endif %}{% endfor %}
{{ found }} {# False — the loop changed a loop-local copy #}
Use a namespace to carry state out of a loop:
{% set ns = namespace(found=false, failed=0) %}
{% for t in tests %}
{% if t.status == "failed" %}
{% set ns.found = true %}
{% set ns.failed = ns.failed + 1 %}
{% endif %}
{% endfor %}
{{ ns.found }} {{ ns.failed }} {# True 1 #}
Better still: compute it without state — {{ tests | selectattr("status", "equalto", "failed") | list | length }} — or pass the number from Python.
Checklist¶
- Dict keys that clash with dict methods (
items,keys,values) use["key"] -
defaultis not expected to replacenonevalues - Loop state goes through
namespace(), not plainset - Whitespace-sensitive output uses
trim_blocksandlstrip_blocks - Literal
{{ }}in the output is wrapped in{% raw %}
See also¶
- Jinja — Templates for Python
- Jinja — Environment, Loaders & Inheritance
- Jinja — Filters, Escaping & Security
- Python Guide for Automation QA