A Django gradebook template loops {% for subject, marks in scores.items %} over a defaultdict and renders nothing; why, and how do you fix it?
answer
- which lookup runs first
- a mapping that never misses
- the key shadows the method
- hand the template a plain dict
basics
~20 sDictionary lookup runs before attribute lookup, so scores.items becomes scores["items"]; a defaultdict creates that key and returns an empty list, so the loop iterates nothing. Convert to a plain dict, or pass pre-built pairs, in the view.
solid answer
~40 sThe Django template engine resolves `scores.items` by trying `scores["items"]` first. On a plain dict that raises `KeyError` and the engine falls back to the `items()` method, but a `defaultdict(list)` never raises: its default factory inserts a new `"items"` key and returns `[]`. The lookup has succeeded, so `items()` is never reached, and the loop runs over an empty list and shows its `{% empty %}` clause or nothing. It also quietly mutates the dictionary. The same shadowing happens with a plain dict that genuinely has a key named `items`, `keys` or `values`, which is common with JSON payloads. Fix it in the view: pass `dict(scores)`, set `scores.default_factory = None`, or better, pass an explicit list such as `sorted(scores.items())` under its own name.
code
python · 10 linesfrom collections import defaultdict
from django.template import Context, Template
scores = defaultdict(list)
scores["maths"].append(91)
t = Template("{% for s, m in scores.items %}{{ s }}={{ m }};{% empty %}none{% endfor %}")
print(t.render(Context({"scores": dict(scores)}))) # maths=[91];
print(t.render(Context({"scores": scores}))) # none
print(sorted(scores)) # ['items', 'maths']go deeper
Remember that {{ d.items }} in a Django template is a dictionary lookup first and a method call only if that fails.
Trace the lookup for a defaultdict step by step and explain why the default factory makes the dictionary step succeed with an empty value.
Diagnose from the symptom: an empty loop with no errors, a stray key after rendering. Fix it by passing explicit rows from the view and pin it with a content assertion.
Use it as an argument for thin, explicit template contexts: views hand templates ready-made rows rather than rich Python structures whose behaviour the lookup rules can reinterpret.
## The symptom A view builds a gradebook with `collections.defaultdict(list)`, appending each student's marks under the subject name, and the template does: ```django {% for subject, marks in scores.items %} <tr><th>{{ subject }}</th><td>{{ marks|join:", " }}</td></tr> {% empty %} <tr><td>No marks recorded.</td></tr> {% endfor %} ``` Every page shows "No marks recorded." although the view clearly has data. No exception, no log line, nothing in the error tracker. ## Why it happens The **Django template language** resolves `scores.items` with its fixed lookup order: 1. **Dictionary lookup**: `scores["items"]`. 2. **Attribute lookup**: `getattr(scores, "items")`, which would find the `items()` method. 3. **List-index lookup**: `scores[int("items")]`. The first step that succeeds wins. For a plain `dict` without an `"items"` key, step 1 raises `KeyError`, step 2 finds the method, and the engine then calls it with no arguments, which yields the key/value pairs. A **`defaultdict`**, however, never raises `KeyError`: its `default_factory` builds a value for any missing key, **stores it**, and returns it. So step 1 succeeds with `[]`, steps 2 and 3 never run, and `{% for %}` iterates an empty list. Django's documentation names this exact case under its description of variable lookups. There is a second, quieter consequence: the lookup **mutates** the dictionary. After the render, `scores` contains an extra `"items": []` entry, which can leak into later code that iterates over it, such as a CSV export built from the same object. ## The same trap without defaultdict The root cause is **dictionary-first lookup**, so any mapping that answers the key shadows the method: | Context value | `{{ data.items }}` resolves to | Loop result | |---|---|---| | plain `dict` without an `"items"` key | the `items()` method, called | the key/value pairs | | `defaultdict(list)` | a new `[]` stored under `"items"` | nothing | | plain `dict` with a key `"items"` (e.g. parsed JSON) | that key's value | whatever that value holds | | a mapping class that answers every key | that answer | depends on the class | Parsed API payloads and `JSONField` values often contain keys such as `items`, `values` or `keys`, which shadow the dict methods in exactly the same way. ## Fixes, in order of preference 1. **Pass exactly what the template needs.** Build the rows in the view: `rows = sorted(scores.items())` and loop `{% for subject, marks in rows %}`. Nothing is left for the lookup rules to reinterpret, and the ordering is explicit. 2. **Convert to a plain dict.** `dict(scores)` removes the default factory, so `scores["items"]` raises `KeyError` and the method is found. This does not help if the data really has an `"items"` key. 3. **Switch the factory off.** Setting `scores.default_factory = None` makes a `defaultdict` raise `KeyError` again for missing keys; it works but is easy to forget and surprising to readers. Avoid "fixing" it by renaming the template call; there is no template syntax that forces attribute lookup or a method call. ## How to diagnose it quickly - **Look for a mapping in the context** whenever a loop over `.items`, `.values` or `.keys` renders empty, and check its type and keys. - **Inspect the object after rendering**: an unexpected `"items"` key in a `defaultdict` is the fingerprint of this bug. - **Add a rendered-content assertion** in the view's test: one expected subject name in the response catches the regression permanently. - **Remember what does not help**: a non-empty `string_if_invalid` marker shows nothing here, because the lookup did not fail; it succeeded with the wrong value. ## Worked fix ```python from collections import defaultdict from django.shortcuts import render from .models import Mark def gradebook(request, student_id): scores = defaultdict(list) for mark in Mark.objects.filter(student_id=student_id): scores[mark.subject].append(mark.value) rows = sorted(scores.items()) # list of (subject, marks) pairs return render(request, "grades/gradebook.html", {"rows": rows}) ``` The template then loops `{% for subject, marks in rows %}`, and the gradebook renders every subject whatever the dictionary type. ## Why interviewers like this one It tests whether a candidate knows the lookup order as a working rule rather than a memorised list. A candidate who can explain that the dictionary step succeeded, and that a successful lookup is invisible to every debugging aid aimed at failed lookups, has usually debugged real templates. The follow-up is often a request to name other mappings or payloads that shadow methods the same way.
- Would setting string_if_invalid to a visible marker have revealed this bug?No. `string_if_invalid` is used only when a lookup fails. Here the dictionary lookup succeeded, returning a freshly created empty list, so the engine had nothing to report. A rendered-content assertion in a test, or inspecting the context object, is what exposes it.
- Why is passing a pre-built list of pairs better than dict(scores)?A list of `(subject, marks)` pairs removes the lookup question entirely, makes the ordering explicit, and still works if the data one day contains a real key named `items`. Converting with `dict()` only removes the default factory; a key that genuinely exists still shadows the method.
saying these in an interview costs you the question
- The template engine calls .items() before trying dictionary lookup
- A defaultdict lookup in a template has no side effects
- string_if_invalid would reveal the empty loop
- Renaming the loop variable fixes the shadowing
- Only defaultdict is affected, never a plain dict