skip to content

In the Django template language, what does the dot in {{ student.marks.0 }} try, in what order, and when does it call something?

level: juniorimportance: must knowfreq 62%

answer

  1. three lookups, short-circuit
  2. mapping before object
  3. numbers only last
  4. callables run with nothing passed

basics

~20 s

Each dot tries a dictionary key, then an attribute or method, then a list index, and keeps the first that works; if the value found is callable, Django calls it with no arguments and uses the result.

solid answer

~40 s

For every dotted segment, Django's `Variable._resolve_lookup` tries `obj["bit"]` first, then `getattr(obj, "bit")`, then `obj[int(bit)]`, and stops at the first success. If all three fail it raises `VariableDoesNotExist` internally and the engine renders its `string_if_invalid` option instead, an empty string by default. After each step a callable result is called with no arguments, so `{{ student.full_name }}` runs the method. The segment after a dot is always a literal name: `{{ marks.subject }}` looks up the key `"subject"`, never the value of a `subject` variable, and there is no syntax for passing arguments. Names that start with an underscore are rejected with `TemplateSyntaxError` when the template compiles.

code

django · 6 lines
django
{# context: {'student': s, 'report': {'term': 'Autumn'}, 'marks': [88, 92]} #}
<h2>{{ student.first_name }} {{ student.last_name }}</h2>
<p>Term: {{ report.term }}</p>
<p>First mark: {{ marks.0 }}</p>
<p>Average: {{ student.average }}</p>
<p>Latest graded: {{ student.marks.all.0 }}</p>

go deeper

for a junior

Recall the order: dictionary key, attribute, list index, first match wins, and that callables are called with no arguments. Be ready to say that {{ a.b }} with no match renders as an empty string.

for a middle

Explain that the segment after a dot is a literal, why integer-keyed dictionaries still resolve, and why a method that needs arguments renders blank rather than failing loudly.

for a senior

Show you have debugged templates in production: related managers needing .all, keys shadowing methods, and the preference for reshaping data in the view over clever template lookups.

for a principal

Frame the lookup rules as a deliberate design choice: templates stay logic-light and forgiving for authors, at the cost of silent errors that teams must catch with review, tests or stricter debugging settings.

## What a template variable is In the **Django template language** (DTL), a **variable** is whatever sits inside `{{ ... }}`. Its value comes from the **context**, the mapping of names to Python objects that the view passes when it renders the template. A variable name may contain letters, digits, underscores and dots. The dot looks like Python attribute access but is not: it is a generic **lookup operator** that the engine resolves at render time by trying several strategies in turn. ## The three lookups, in order For each segment after a dot, `django.template.base.Variable._resolve_lookup` tries: 1. **Dictionary lookup**: `current["bit"]`, attempted only when the object's type implements `__getitem__`. 2. **Attribute lookup**: `getattr(current, "bit")`, which also finds methods, properties and model fields. 3. **List-index lookup**: `current[int("bit")]`, which serves lists, tuples, QuerySets and, as a side effect, dictionaries keyed by integers. The logic is **short-circuit**: the first lookup that succeeds wins and the rest are never tried. If all three fail, the engine raises `VariableDoesNotExist` internally and renders the engine's `string_if_invalid` option in place of the variable, an empty string unless you configure otherwise. | Template expression | Context value | Lookup that succeeds | Output | |---|---|---|---| | `{{ report.term }}` | `{"term": "Autumn"}` | dictionary | `Autumn` | | `{{ student.last_name }}` | a model instance | attribute | the field value | | `{{ marks.0 }}` | `[88, 92]` | list index | `88` | | `{{ by_year.2024 }}` | `{2024: "B+"}` | list index, via `int("2024")` | `B+` | | `{{ student.average }}` | method `average(self)` | attribute, then called | the return value | The integer-keyed dictionary row surprises people: the dictionary lookup with the string `"2024"` fails, attribute lookup fails, and the list-index step converts the segment to the integer `2024`, which the dictionary happens to accept. ## Callables are called, with no arguments After every step, if the current value is **callable**, the engine calls it with **no arguments** and continues with the return value. That is why `{{ student.get_absolute_url }}` and `{{ student.courses.all }}` work without parentheses. There is no way to pass an argument from a template: if the callable requires one, the call fails and the variable renders as `string_if_invalid`. Two attributes on a callable change this rule: `alters_data = True` blocks the call for methods with side effects, and `do_not_call_in_templates = True` leaves the object uncalled so its own attributes can be reached. ## The segment is always a literal The text after a dot is taken literally. With `subject = "math"` in the context, `{{ marks.subject }}` looks for a key or attribute named `subject`, not `math`. The template language has no syntax for a dynamic key. The usual fixes are to reshape the data in the view, for example a list of `(subject, mark)` pairs to loop over, or to write a small custom filter, a mechanism that belongs to template filters rather than to lookup. ## Traps worth knowing - **Managers are not indexable.** On a reverse foreign key, `{{ student.marks.0 }}` renders nothing, because a related manager supports neither `["0"]` nor `[0]`. Write `{{ student.marks.all.0 }}`: `all` is called, and the resulting QuerySet accepts an integer index. - **Dictionary-first order means keys shadow methods.** If a dictionary holds a key called `items`, `{{ data.items }}` returns that key's value, not the `items()` method. - **Underscores are refused.** `{{ student._state }}` raises `TemplateSyntaxError` ("Variables and attributes may not begin with underscores") when the template is compiled, not when it renders. - **Built-in names.** Every context contains `True`, `False` and `None`. - **Numbers are literals.** A bare `{{ 3 }}` or `{{ 2.5 }}` is a number, not a lookup, and the characters `+` and `-` are rejected inside variable names. ## A gradebook worked example ```python from django.template import Context, Template class Student: first_name = "Ada" def average(self): return 91.5 def mark_for(self, subject): return 95 t = Template( "{{ s.first_name }} | {{ s.average }} | {{ s.mark_for }} | " "{{ report.term }} | {{ marks.1 }}" ) print(t.render(Context({ "s": Student(), "report": {"term": "Autumn"}, "marks": [88, 92], }))) # Ada | 91.5 | | Autumn | 92 ``` `mark_for` needs an argument, so it renders as the empty `string_if_invalid`; everything else resolves through one of the three lookups. The engine does not warn you about that blank, which is exactly why interviewers follow this question with one about silent failure.

  • How would you show the mark for a subject whose name is held in another context variable?
    The template language cannot use a variable as a lookup key, because the segment after a dot is always literal. Reshape the data in the view, for example a list of `(subject, mark)` pairs or the single value you need, or write a small custom filter that performs the lookup. Reshaping in the view is usually clearer and easier to test.
  • Why does {{ by_year.2024 }} work on a dictionary keyed by integers?
    The dictionary lookup tries the string key `"2024"` and fails, attribute lookup fails, and the third step, list-index lookup, converts the segment with `int()` and indexes with `2024`. A dictionary with an integer key accepts that, so the value comes back even though list-index lookup was meant for sequences.
  • When is an underscore-prefixed name like {{ student._state }} rejected?
    At compile time. When the template is parsed, the variable constructor raises `TemplateSyntaxError` with the message that variables and attributes may not begin with underscores, so the template never renders at all. This keeps private and internal attributes out of reach of template authors.

A school clerk asked for 'student.marks' checks the labelled folder tabs first (dictionary keys), then the student's own record card (attributes), then the numbered slots (list index), and hands over the first match. If what the clerk finds is a 'calculate' button, the clerk presses it, but only if it needs no settings chosen.

saying these in an interview costs you the question

  • The dot is plain Python attribute access and nothing else
  • Attribute lookup is tried before dictionary lookup
  • {{ marks.subject }} uses the value of the subject variable as the key
  • You can pass arguments to a method from the template with parentheses
  • {{ student.marks.0 }} indexes a reverse foreign key directly