In Django templates, how does {% for %} work together with {% empty %} and the forloop variable, such as forloop.counter and forloop.last?
answer
- logic goes in percent braces
- a clause for nothing to show
- a per-iteration dictionary
- one-based and zero-based counters
basics
~20 s{% for item in items %} repeats its body per element; an {% empty %} clause renders instead when the sequence is empty or missing. Inside, forloop exposes counter, counter0, revcounter, revcounter0, first, last, length and parentloop.
solid answer
~40 sIn the Django template language, `{{ }}` prints a value and `{% %}` runs a tag, and `{% for %}` is the loop tag: `{% for article in articles %}...{% endfor %}`. An optional `{% empty %}` clause renders when the sequence is empty, `None` or not in the context, which replaces a wrapping `{% if articles %}`. Inside the loop the `forloop` dictionary gives `counter` (from 1), `counter0` (from 0), `revcounter` and `revcounter0` (counting down), `first` and `last` booleans, `parentloop` for the enclosing loop, and, since Django 6.0, `length`. The tag also supports `reversed` and unpacking, as in `{% for key, value in data.items %}`. There is no break or continue; filter the data in the view instead.
code
django · 5 lines{% for tag in article.tag_names %}
<a href="/tags/{{ tag }}/">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
{% empty %}
<span>untagged</span>
{% endfor %}go deeper
Recall {{ }} for output and {% %} for logic, write a for loop with an empty clause, and use forloop.counter, first and last for numbering and separators.
Explain counter versus counter0, revcounter, parentloop in nested loops, unpacking pairs, and why an empty clause also covers None and missing names.
Keep loops cheap: paginate or slice in the view, remember the tag materialises the whole sequence, and move filtering out of templates since there is no break.
Hold the line that templates iterate prepared data: views shape lists, templates present them, and that split keeps rendering predictable and testable.
## Tags versus variables The **Django template language** (DTL) has three kinds of markup: - **Variables**, `{{ article.title }}`, print a value. - **Tags**, `{% for %}`, `{% if %}`, `{% url %}`, run logic: loops, conditions, links, loading libraries. Block tags such as `{% for %}` need a matching end tag. - **Comments**, `{# single line #}` or `{% comment "why" %}...{% endcomment %}` for several lines, produce no output. `{# #}` cannot span a newline, and `{% comment %}` blocks cannot be nested. ## The loop and its empty clause ```django <ul> {% for article in articles %} <li>{{ forloop.counter }}. {{ article.title }}</li> {% empty %} <li>No articles yet.</li> {% endfor %} </ul> ``` `{% for %}` resolves the sequence once, then renders its body per element with the loop variable bound. The **`{% empty %}` clause** renders instead of the body when there is nothing to loop over: an empty list or QuerySet, `None`, or a name that is missing from the context (the tag treats an unresolvable variable as `None`). It is the idiomatic replacement for: ```django {% if articles %}{% for article in articles %}...{% endfor %}{% else %}No articles yet.{% endif %} ``` Useful variations: 1. **Reverse iteration**: `{% for article in articles reversed %}`. 2. **Unpacking**: `{% for title, url in links %}` for a list of pairs, or `{% for key, value in data.items %}` for a dictionary. 3. **Filters on the sequence**: `{% for tag in tags|dictsort:"name" %}`. ## The `forloop` variable Inside the loop Django pushes a dictionary named `forloop` onto the context: | Key | Meaning | First of three items | |---|---|---| | `forloop.counter` | position, starting at 1 | `1` | | `forloop.counter0` | position, starting at 0 | `0` | | `forloop.revcounter` | items remaining, ending at 1 | `3` | | `forloop.revcounter0` | items remaining, ending at 0 | `2` | | `forloop.first` | `True` on the first pass | `True` | | `forloop.last` | `True` on the last pass | `False` | | `forloop.length` | total number of items (Django 6.0+) | `3` | | `forloop.parentloop` | the enclosing loop's `forloop`, in nested loops | `{}` at top level | Typical uses: numbering rows, adding a separator except after the last item (`{% if not forloop.last %}, {% endif %}`), marking the first item as active, or reaching the outer counter in a nested table with `forloop.parentloop.counter`. ## What the DTL deliberately lacks - **No `break` or `continue`.** The loop always visits every element. Filter or slice the data in the view, or use a filter such as `slice`, instead of trying to stop early. - **No arbitrary Python.** You cannot call a method with arguments or build a list inline; the loop consumes what the view provides. - **No loop-local assignment**, other than what `{% with %}` or a custom tag provides. These limits are intentional: templates stay declarative, and data shaping stays in Python where it can be tested. ## Behaviour worth knowing - **The sequence is materialised.** To know the length, the tag converts anything without `__len__` into a list, and a QuerySet is fully evaluated before the first iteration. A loop over a large QuerySet therefore loads every row it is given; paginate in the view. - **Nested loops shadow `forloop`.** The inner loop's `forloop` hides the outer one, which is why `parentloop` exists. - **Unpacking must match.** If the number of loop variables does not match an item's length, the tag raises an error rather than silently skipping. - **Scope.** The loop variable and `forloop` exist only between `{% for %}` and `{% endfor %}`. ## Putting it together ```django <table> {% for author, posts in authors_with_posts %} {% for post in posts %} <tr class="{% if forloop.first %}first{% endif %}"> <td>{{ forloop.parentloop.counter }}.{{ forloop.counter }}</td> <td>{{ author }}</td> <td>{{ post.title }}</td> </tr> {% empty %} <tr><td colspan="3">{{ author }} has not published yet.</td></tr> {% endfor %} {% endfor %} </table> ``` The outer loop unpacks pairs prepared in the view, the inner loop numbers rows with both counters, and the inner `{% empty %}` handles authors with no posts without an extra `{% if %}`.
- How do you stop a Django template loop after the first five items?The DTL has no break tag. Limit the data instead: slice in the view (`articles[:5]`, which becomes a `LIMIT` for a QuerySet) or use the `slice` filter, `{% for a in articles|slice:":5" %}`. Slicing in the view is clearer and avoids loading rows you will not show.
- When does {% empty %} render for a name that is not in the context?Always: the `for` tag resolves its sequence with failures ignored, so a missing name becomes `None`, which the tag treats as an empty list and renders the `{% empty %}` clause. That is convenient, but it also means a misspelled sequence name shows the empty message rather than an error.
saying these in an interview costs you the question
- forloop.counter starts at 0
- {% empty %} only renders for an empty list, never for None
- You can leave a Django template loop early with {% break %}
- forloop.last is the last item itself rather than a boolean
- Nested loops share one forloop, so the outer counter is lost