skip to content

In Django templates, how does {% for %} work together with {% empty %} and the forloop variable, such as forloop.counter and forloop.last?

level: juniorimportance: must knowfreq 62%

answer

  1. logic goes in percent braces
  2. a clause for nothing to show
  3. a per-iteration dictionary
  4. 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 s

In 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
django
{% for tag in article.tag_names %}
  <a href="/tags/{{ tag }}/">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
{% empty %}
  <span>untagged</span>
{% endfor %}

go deeper

for a junior

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.

for a middle

Explain counter versus counter0, revcounter, parentloop in nested loops, unpacking pairs, and why an empty clause also covers None and missing names.

for a senior

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.

for a principal

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