Loop and condition in Jinja

Learn to use loops and conditions in Jinja templates for dynamic web pages. This lesson covers core concepts, hands-on examples, and common pitfalls.

Focus: loop and condition in jinja templates

Sponsored

You’ve built a Flask view that returns a variable, but the moment you need to render a list of users, filter by login status, or show an empty state, your template hits a wall. Manually looping in Python and concatenating HTML strings is brittle, unsafe, and impossible to maintain — and that’s exactly the pain this lesson solves: how to use the loop and condition in Jinja templates to turn static pages into dynamic, data-driven interfaces with clean, readable syntax.

The problem this lesson solves

Every real web app has repetitive rendering needs: a table of orders, a list of comments, a navigation menu, or a dashboard of metrics. Without template logic, you’d either copy-paste the same HTML block dozens of times or build strings in Python with ''.join(...). Both approaches fail:

  • Copy-paste HTML means every new item requires editing the page source — one typo and the layout breaks.
  • String concatenation in Python mixes presentation with logic, makes escaping user input your job, and turns a simple view into a mess of f"<tr>{row}</tr>".
  • No empty-state handling means a list with zero items shows a blank page instead of a helpful message.

Jinja’s loop and condition in Jinja templates solves all of this by putting control flow inside the template, where it belongs. You loop over a list passed from the view, apply conditions per item, and provide fallbacks — all without leaving the HTML context.

Core concept / mental model

Think of a Jinja template as a stencil and the data from your view as the ink. The stencil has cutouts (variables, loops, conditionals) that the ink fills in. Loops are like moving a rubber stamp across a page — each pass stamps the same shape but with different data. Conditions are like filters on a camera: they decide which stamps get applied and which are skipped.

Two core Jinja tags drive this:

  • {% for %} — iterates over a Python iterable (list, dict, tuple, generator).
  • {% if %} — evaluates a Python expression and includes or excludes the block.

Unlike Python, Jinja has no indentation-based blocks — you must close every {% for %} with {% endfor %} and every {% if %} with {% endif %}. A mental model that helps: Jinja is a mini-language that mirrors Python’s control flow but with explicit delimiters and a context-only namespace. You can use loop.index, loop.first, and loop.last inside a loop, and full Python expressions (like user.is_admin or items|length == 0) inside conditions.

How it works step by step

1. Set up your Flask app and template

Your Flask view creates a list of dictionaries (or objects) and passes it to render_template. The template then uses {% for %} to iterate and {% if %} to branch.

2. Write a basic loop

The simplest usage is a list of strings:

# app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    users = ['Alice', 'Bob', 'Charlie']
    return render_template('users.html', users=users)
<!-- users.html -->
<ul>
{% for user in users %}
    <li>{{ user }}</li>
{% endfor %}
</ul>

Output:

<ul>
    <li>Alice</li>
    <li>Bob</li>
    <li>Charlie</li>
</ul>

3. Add a condition inside the loop

Now refine it — highlight admins:

<ul>
{% for user in users %}
    {% if user.is_admin %}
        <li class="admin">{{ user.name }} (Admin)</li>
    {% else %}
        <li>{{ user.name }}</li>
    {% endif %}
{% endfor %}
</ul>

Each user is rendered only if the condition passes, otherwise the else branch runs.

4. Use loop variables for numbered lists or zebra striping

Jinja provides a loop object with useful helpers — index, index0, first, last, length.

<ol>
{% for item in items %}
    <li>{{ loop.index }}. {{ item.name }}</li>
{% endfor %}
</ol>

For alternating row colors:

<tr class="{{ 'even' if loop.index is even else 'odd' }}">
    <td>{{ item }}</td>
</tr>

5. Handle the empty case with else

Every {% for %} can have an {% else %} block that runs when the iterable is empty — no need to check length manually.

<ul>
{% for user in users %}
    <li>{{ user.name }}</li>
{% else %}
    <li>No users found.</li>
{% endfor %}
</ul>

Hands-on walkthrough

Let’s build a complete mini-app: a product listing page that shows a message when stock is low, marks out-of-stock items, and handles an empty cart. Create a products.py file and a templates/products.html template.

Step 1 — The Flask view:

from flask import Flask, render_template

app = Flask(__name__)

products = [
    {'name': 'Laptop', 'stock': 5},
    {'name': 'Mouse', 'stock': 0},
    {'name': 'Keyboard', 'stock': 2},
]

@app.route('/products')
def product_list():
    return render_template('products.html', products=products)

Step 2 — The template with loops and conditions:

<!DOCTYPE html>
<html>
<head><title>Product List</title></head>
<body>
  <h1>Our Products</h1>
  {% if products %}
    <table>
      <tr><th>#</th><th>Name</th><th>Stock</th><th>Status</th></tr>
      {% for product in products %}
        <tr>
          <td>{{ loop.index }}</td>
          <td>{{ product.name }}</td>
          <td>{{ product.stock }}</td>
          <td>
            {% if product.stock == 0 %}
              <span class="out-of-stock">Out of stock</span>
            {% elif product.stock < 3 %}
              <span class="low-stock">Low stock</span>
            {% else %}
              <span class="in-stock">In stock</span>
            {% endif %}
          </td>
        </tr>
      {% endfor %}
    </table>
  {% else %}
    <p>No products to display.</p>
  {% endif %}
</body>
</html>

Step 3 — Run and observe:

python products.py

Open http://127.0.0.1:5000/products. You’ll see:

  • Row 1: #1 / Laptop / 5 / In stock
  • Row 2: #2 / Mouse / 0 / Out of stock
  • Row 3: #3 / Keyboard / 2 / Low stock

Notice how loop.index generates sequential numbers, and the if/elif/else chain picks the correct CSS class. This is the full power of the loop and condition in Jinja templates.

Compare options / when to choose what

Jinja loops are not the only way to render repeated content. Here’s a comparison:

Approach Use case Pros Cons
Jinja {% for %} HTML blocks with mixed markup Clear, secure autoescaping, template logic stays in template Slightly slower than raw string concat
Python ''.join() Micro-rendering simple strings Fast, no template dependency Mixes logic and presentation, error-prone with HTML escaping
JavaScript fetch + DOM Client-side dynamic updates No page reload, async Adds API layer, SEO weaker, extra complexity

When to choose: Use Jinja loops for server-rendered pages that don’t need real-time updates. Choose client-side rendering only when you need interactivity without navigation. For most small-to-mid Flask apps, Jinja is the right call.

Variations: you can also use Jinja’s for with a dict to iterate over key-value pairs ({% for key, value in dict.items() %}), and you can apply filters in the loop, like {% for item in items|sort %} — useful when you want ordering without modifying the view.

Troubleshooting & edge cases

Symptom: “Undefined” printed in the loop — Your variable is missing or the key is misspelled. Check the view passes the right name and that the dictionary keys match the template references. Use {% if product is defined %} to guard.

Symptom: {% else %} inside {% for %} fires even when the list has items — Make sure you put the {% else %} directly after the loop body, before {% endfor %}. If you accidentally place it outside, Jinja will treat it as the if’s else instead.

Symptom: loop.index starts at 0 instead of 1 — That’s because loop.index0 exists for zero-based counting, but loop.index always starts at 1. If you need a zero-based counter for array indexing, use loop.index0.

Symptom: White spaces or blank lines appear in output — Jinja preserves literal whitespace. To strip whitespace around tags, you can add a minus sign: {%- for item in items -%}. This trims the line break before and after the block.

Edge case: Modifying the list while iterating — Jinja doesn’t allow you to change the iterable during a loop. If you need to filter items, pass a pre-filtered list from the view or use the selectattr filter.

Edge case: Nesting loops and conditions — You can nest arbitrarily, but keep readability. Use {% if %} inside {% for %} to per-item logic, and a second {% for %} for sub-lists.

What you learned & what's next

You’ve mastered the core of loop and condition in Jinja templates: using {% for %} to iterate over data, {% if %} to branch, loop variables for indexing and flags, and else blocks for empty states. You also learned how to compare with client-side alternatives and handle common pitfalls like undefined variables and whitespace.

This is a foundational step for your Python web development journey. Next in the track, you’ll learn to leverage template inheritance with {% extends %} and {% block %} to build reusable layouts, and then move to form handling and validation with Flask-WTF. Each lesson builds on these template skills, letting you build interactive, data-driven web apps with confidence.

Practice recap

Create a small Flask app with a list of blog posts (title, author, published date). Render them in a table using for and apply if to show 'Draft' vs 'Published' status based on a boolean. Use loop.index to number the rows, and add an else block to display 'No posts yet' when the list is empty. Experiment with adding a nested loop for tags on each post.

Common mistakes

  • Forgetting to close {% for %} with {% endfor %} — Jinja raises a TemplateSyntaxError.
  • Using Python’s for/else semantics incorrectly — Jinja’s {% else %} after a loop runs only if the iterable is empty, not after a break.
  • Referencing a nonexistent variable in a loop condition — Jinja prints Undefined or silently skips; always use is defined or default().
  • Overusing loop.index0 when loop.index is clearer — mixing up the two leads to off-by-one errors in numbering.

Variations

  1. Use {% for key, value in dict.items() %} when iterating over a dictionary to get both keys and values.
  2. Apply filters like |sort or |reverse directly in the loop declaration to alter iteration order without changing the view.
  3. Combine {% if %} with filters like |length to check emptiness: {% if items|length == 0 %} instead of {% for ... else %}.

Real-world use cases

  • Rendering a dynamic dashboard where each metric card is generated via a for loop and styled differently based on thresholds (e.g., high/medium/low risk).
  • Displaying a user comment section where each comment gets a moderator badge if the commenter is an admin, using if inside the loop.
  • Building an e-commerce product grid that shows a 'sold out' overlay on items with zero stock and an 'only X left' badge for low stock, all via elif conditions.

Key takeaways

  • Use {% for %} to iterate over any Python iterable in a Jinja template and always close it with {% endfor %}.
  • Leverage the loop object (index, first, last, length) to handle numbering and edge cases within loops.
  • Add an {% else %} block after a for loop to provide a friendly empty-state message when the list has no items.
  • Use {% if %}, {% elif %}, and {% else %} to conditionally render HTML based on data values or loop position.
  • Keep presentation logic in the template — don’t build HTML strings in Python; Jinja autoescapes and keeps your code maintainable.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.