Chapter 4: Django for Tag
for Tag: {% for %} — the loop tag.
This tag appears in almost every real template you will ever write.
Many beginners write it once or twice, see it “kind of works”, and never really understand its full power, its special variables, its traps, and how to make it beautiful and maintainable.
Today I’m going to teach you {% for %} the way a good senior would teach it: slowly, with real examples from your polls app, showing every useful pattern, every special variable, every common mistake, and how professionals actually write loops in 2026.
We will build everything together — step by step — so that after this lesson you can confidently write any kind of loop you will ever need.
1. The Absolute Simplest Form (What Everyone Starts With)
|
0 1 2 3 4 5 6 7 8 9 10 |
<ul> {% for question in latest_questions %} <li>{{ question.question_text }}</li> {% endfor %} </ul> |
That’s it.
- latest_questions must be a list, queryset, or any iterable passed from the view
- Inside the loop you get access to the current item (question)
- After the loop ends → no more items → loop finishes
2. The {% empty %} Clause — Very Important in Real Projects
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<ul> {% for question in latest_questions %} <li> <a href="{% url 'polls:detail' slug=question.slug %}"> {{ question.question_text }} </a> ({{ question.vote_count|pluralize:"vote,vote" }}) </li> {% empty %} <li style="color:#666; font-style:italic; padding: 2rem 0;"> No active polls available right now… <br> Why not <a href="{% url 'admin:polls_question_add' %}">create the first one</a>? </li> {% endfor %} </ul> |
Why {% empty %} is golden:
- Handles the “no items” case gracefully
- Prevents ugly empty <ul></ul> or broken layout
- Used in 90%+ of real list templates
3. All Built-in Loop Variables (Memorize These)
Inside any {% for %} loop Django gives you these special variables automatically:
| Variable | What it gives | Example output (when 5 items) |
|---|---|---|
| forloop.counter | 1-based index | 1, 2, 3, 4, 5 |
| forloop.counter0 | 0-based index | 0, 1, 2, 3, 4 |
| forloop.revcounter | Countdown from end (1-based) | 5, 4, 3, 2, 1 |
| forloop.revcounter0 | Countdown from end (0-based) | 4, 3, 2, 1, 0 |
| forloop.first | True only on first iteration | True, False, False… |
| forloop.last | True only on last iteration | False, False, …, True |
| forloop.parentloop | Access parent loop in nested loops | See nested example below |
4. Real-World Examples Using Loop Variables
Example A: Numbered list with “Featured” on first item
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<ol> {% for question in latest_questions %} <li> {% if forloop.first %} <strong style="color:#d32f2f;">Featured:</strong> {% endif %} {{ forloop.counter }}. <a href="{% url 'polls:detail' slug=question.slug %}"> {{ question.question_text }} </a> </li> {% empty %} <p>No polls yet…</p> {% endfor %} </ol> |
Example B: Zebra striping (alternating row colors)
|
0 1 2 3 4 5 6 7 8 9 10 |
{% for question in latest_questions %} <div class="{% if forloop.counter0|divisibleby:2 %}even-row{% else %}odd-row{% endif %}"> {{ question.question_text }} </div> {% endfor %} |
Or simpler with forloop.counter0:
|
0 1 2 3 4 5 6 |
<div class="row-{{ forloop.counter0|add:1 }}">…</div> |
Example C: Show separator only between items (not after last)
|
0 1 2 3 4 5 6 7 8 9 10 11 |
{% for question in latest_questions %} {{ question.question_text }} {% if not forloop.last %} <span style="color:#ccc;"> • </span> {% endif %} {% endfor %} |
Example D: Nested loops (very common with related objects)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{% for question in latest_questions %} <h3>{{ question.question_text }}</h3> <ul> {% for choice in question.choices.all %} <li> {{ forloop.parentloop.counter }}.{{ forloop.counter }} {{ choice.choice_text }} ({{ choice.votes }} vote{{ choice.votes|pluralize }}) </li> {% empty %} <li style="color:#888;">No choices yet</li> {% endfor %} </ul> {% endfor %} |
→ forloop.parentloop.counter gives you the outer question number
5. Common Beginner Mistakes & How to Fix Them
| Mistake | What happens | Correct way |
|---|---|---|
| {% for question in questions %} → nothing shows | Forgot to pass latest_questions from view | Check view: context = {“latest_questions”: qs} |
| Empty list shows ugly blank space | No {% empty %} | Always add {% empty %} block |
| {{ forloop.counter }} shows nothing | Typo: forloop (no space) | It’s forloop — one word |
| Want reverse order but don’t know how | — | Sort in view: .order_by(‘-pub_date’) |
| Nested loop shows wrong number | Using forloop.counter instead of parentloop | Use forloop.parentloop.counter |
6. Pro Tips Used in Real 2026 Projects
- Always add {% empty %} — even if just <p>No items</p>
- Use forloop.first for special first-item styling
- Use forloop.last to avoid trailing commas/separators
- Prefer view logic for complex sorting/filtering — keep template loops simple
- If loop is very long → add {% if forloop.counter0 < 20 %} … pagination in view is better
Your Quick Practice Task (Do This Right Now)
Open polls/templates/polls/index.html and improve your question loop:
- Add {{ forloop.counter }}. before each question
- Add {% if forloop.first %}<strong>Newest:</strong>{% endif %}
- Add {% empty %} message with link to admin
- Add separator only between items (use {% if not forloop.last %}<hr>{% endif %})
- Try nested choice loop inside one question (show choices with forloop.parentloop.counter)
Tell me what you want next:
- Which part of {% for %} is still confusing? (loop variables? empty? nested?)
- Want 10 more real-life {% for %} examples from different apps?
- Ready to learn {% regroup %} (grouping items in loop)?
- Or ready to move to Django Forms + Voting + POST handling?
You’re getting very strong with template logic — this {% for %} tag is used on almost every page you’ll ever build.
Keep practicing — you’re doing excellent! 🚀🇮🇳
