Skip to content

Control Structures (If/Else, Loops)

Control structures are how you build dynamic pages.

html
{% if user %}
  <p>Hello, {{ user.username }}!</p>
{% elif guest_name %}
  <p>Hello, {{ guest_name }}!</p>
{% else %}
  <p>Hello, stranger!</p>
{% endif %}
html
<ul>
  {% for item in items %}
    <li>{{ item }}</li>
  {% endfor %}
</ul>

Jinja provides loop inside loops:

  • loop.index (1-based)
  • loop.index0 (0-based)
  • loop.first, loop.last
html
{% for user in users %}
  <p>{{ loop.index }}. {{ user.username }}</p>
{% endfor %}
html
{% if users %}
  <!-- show list -->
{% else %}
  <p>No users found.</p>
{% endif %}

Prefer to compute complex values in Python and pass them in.

Templates should be readable.

Inside {% for %} Jinja2 gives you a loop variable with everything you usually need, so you rarely have to track an index yourself:

diagram Diagram mermaid

Measured:

template
{% for x in items %}{{ loop.index }}:{{ x }}{{ ',' if not loop.last }}{% endfor %}
items = ['a','b','c']
1:a,2:b,3:c

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

empty.html
{% 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:

results.html
{% for post in posts %}
  <article>{{ post.title }}</article>
{% else %}
  <p>No posts yet.</p>
{% endfor %}
conditions.html
{% 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.

sketch The loop variable, step by step p5.js
loop.index is 1-based, loop.first and loop.last mark the ends, and the else branch runs only when the sequence is empty.
pch.quizTag pch.quizDefaultTitle
  1. In Jinja2, when does the {% else %} branch of a {% for %} run?

    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.

  2. For items = ['a','b','c'], what does {{ loop.index }} produce on the first iteration?

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

  3. Which is the idiomatic way to join items with commas and no trailing comma?

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

pch.feedbackHeading

pch.feedbackSubheading