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
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 aTemplateSyntaxError. - Using Python’s
for/elsesemantics incorrectly — Jinja’s{% else %}after a loop runs only if the iterable is empty, not after abreak. - Referencing a nonexistent variable in a loop condition — Jinja prints
Undefinedor silently skips; always useis definedordefault(). - Overusing
loop.index0whenloop.indexis clearer — mixing up the two leads to off-by-one errors in numbering.
Variations
- Use
{% for key, value in dict.items() %}when iterating over a dictionary to get both keys and values. - Apply filters like
|sortor|reversedirectly in the loop declaration to alter iteration order without changing the view. - Combine
{% if %}with filters like|lengthto 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
forloop 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
ifinside 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
elifconditions.
Key takeaways
- Use
{% for %}to iterate over any Python iterable in a Jinja template and always close it with{% endfor %}. - Leverage the
loopobject (index,first,last,length) to handle numbering and edge cases within loops. - Add an
{% else %}block after aforloop 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.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.