Control Structures (If/Else, Loops)
Control structures are how you build dynamic pages.
If / elif / else
Section titled “If / elif / else”{% if user %}
<p>Hello, {{ user.username }}!</p>
{% elif guest_name %}
<p>Hello, {{ guest_name }}!</p>
{% else %}
<p>Hello, stranger!</p>
{% endif %}For loops
Section titled “For loops”<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>Loop helpers
Section titled “Loop helpers”Jinja provides loop inside loops:
loop.index(1-based)loop.index0(0-based)loop.first,loop.last
{% for user in users %}
<p>{{ loop.index }}. {{ user.username }}</p>
{% endfor %}Handling empty lists
Section titled “Handling empty lists”{% if users %}
<!-- show list -->
{% else %}
<p>No users found.</p>
{% endif %}Keep logic light
Section titled “Keep logic light”Prefer to compute complex values in Python and pass them in.
Templates should be readable.
Loops carry a loop object
Section titled “Loops carry a loop object”Inside {% for %} Jinja2 gives you a loop variable with everything you usually need,
so you rarely have to track an index yourself:
flowchart TD
F["{% for x in items %}"] --> L["loop.index 1-based
loop.index0 0-based
loop.first / loop.last
loop.length
loop.revindex"]
F --> E["{% else %}
runs when items is EMPTY"]
L --> B["{% endfor %}"]
E --> B
Measured:
{% for x in items %}{{ loop.index }}:{{ x }}{{ ',' if not loop.last }}{% endfor %}1:a,2:b,3:cloop.index is 1-based; use loop.index0 when you need to match a Python index.
The {{ ',' if not loop.last }} idiom is the clean way to join with separators without a
trailing comma.
for ... else runs on an empty sequence
Section titled “for ... else runs on an empty sequence”{% for x in [] %}{{ x }}{% else %}EMPTY{% endfor %}renders EMPTY. This is not Python’s for/else, which runs when the loop completes
without break. In Jinja2 it means “the sequence had no items”, which is exactly the
“no results” case every list page needs:
{% for post in posts %}
<article>{{ post.title }}</article>
{% else %}
<p>No posts yet.</p>
{% endfor %}Conditionals and the inline form
Section titled “Conditionals and the inline form”{% if user.is_admin %}
<a href="{{ url_for('admin') }}">Admin</a>
{% elif user.is_staff %}
<a href="{{ url_for('staff') }}">Staff</a>
{% else %}
<a href="{{ url_for('profile') }}">Profile</a>
{% endif %}
<span class="{{ 'active' if page == current else '' }}">...</span>The inline {{ a if cond else b }} is an expression, so it belongs inside {{ }}.
Jinja2 supports and, or, not, in, and chained comparisons, but deliberately does
not allow arbitrary Python — that pressure is intentional. If a condition needs real
logic, compute it in the view and pass a boolean.
See it move
Section titled “See it move”Check yourself
Section titled “Check yourself”-
In Jinja2, when does the {% else %} branch of a {% for %} run?
Measured: {% for x in [] %}{{ x }}{% else %}EMPTY{% endfor %} renders 'EMPTY'. This is the opposite of Python's for/else, which runs when the loop was not broken out of.
pch.quizShowAnswer
B — when the sequence is empty, so the body never ran — Measured: {% for x in [] %}{{ x }}{% else %}EMPTY{% endfor %} renders 'EMPTY'. This is the opposite of Python's for/else, which runs when the loop was not broken out of.
-
For items = ['a','b','c'], what does {{ loop.index }} produce on the first iteration?
loop.index is 1-based; loop.index0 is the 0-based version. Measured output was '1:a,2:b,3:c'.
pch.quizShowAnswer
B — 1 — loop.index is 1-based; loop.index0 is the 0-based version. Measured output was '1:a,2:b,3:c'.
-
Which is the idiomatic way to join items with commas and no trailing comma?
loop.last is True on the final iteration, so the separator is emitted for every item except that one. Measured: '1:a,2:b,3:c'.
pch.quizShowAnswer
A — {{ ',' if not loop.last }} — loop.last is True on the final iteration, so the separator is emitted for every item except that one. Measured: '1:a,2:b,3:c'.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading