skip to content

Why does a Django template render a misspelled {{ studnet.name }} as an empty string, and how do you catch such typos?

level: middleimportance: must knowfreq 55%

answer

  1. forgiving by design
  2. an engine option, empty by default
  3. tags treat it as None
  4. a DEBUG-level logger

basics

~20 s

A failed lookup is swallowed on purpose: Django renders the engine's string_if_invalid option, an empty string by default. To catch typos, temporarily set string_if_invalid to a visible marker, read the django.template DEBUG log, or assert rendered output in tests.

solid answer

~40 s

When all three lookups fail, Django raises `VariableDoesNotExist` internally and replaces the variable with the engine's `string_if_invalid` option, `''` by default, so templates degrade gracefully when optional data is missing. Inside `{% if %}`, `{% for %}` and `{% regroup %}` an invalid variable is treated as `None`. To find a typo, set `"string_if_invalid": "MISSING: %s"` in `TEMPLATES[...]["OPTIONS"]` while debugging (the `%s` becomes the variable name), or turn the `django.template` logger up to `DEBUG`, which logs every failed lookup. The docs warn against leaving a non-empty value on permanently: many templates, some of Django's own included, rely on the silence, and a non-empty value also skips any filters on the invalid variable. Exceptions raised inside a called method are different: they propagate unless flagged as silent.

code

python · 11 lines
python
from django.template import Context, Engine

lax = Engine()
strict = Engine(string_if_invalid="MISSING: %s")
src = "[{{ studnet.name }}] [{{ nickname|default:'n/a' }}]"
ctx = {"student": {"name": "Ada"}}

print(lax.from_string(src).render(Context(ctx)))
# [] [n/a]
print(strict.from_string(src).render(Context(ctx)))
# [MISSING: studnet.name] [MISSING: nickname]

go deeper

for a junior

Know that a misspelled variable renders as an empty string by default, and that the page still renders. Say that this is on purpose, not a bug.

for a middle

Explain string_if_invalid, its %s substitution, why it is a temporary debugging aid, and how if, for and regroup treat an invalid variable as None instead.

for a senior

Describe a durable defence: content assertions in view tests, the django.template DEBUG logger in development, and the difference between a failed lookup and an exception raised by called code.

for a principal

Weigh forgiving templates against correctness: decide which pages deserve rendered-content tests, and how the team keeps context names explicit so silent blanks do not reach users.

## The behaviour In the **Django template language**, a variable whose lookup fails does not raise an error. If `studnet` is not in the context, `{{ studnet.name }}` renders as nothing and the rest of the page renders normally. Internally, `Variable._resolve_lookup` tries a dictionary key, an attribute and a list index for each dotted segment; when all three fail it raises `VariableDoesNotExist`, and `FilterExpression.resolve` catches that exception and substitutes the engine's **`string_if_invalid`** option. `string_if_invalid` is an option of the `DjangoTemplates` backend, set in `TEMPLATES[...]["OPTIONS"]`. Its default is the empty string `''`, which is why a typo is invisible. ## Why Django chose silence - **Templates are meant for presentation**, often written by people who are not the Python authors, and the language is kept deliberately forgiving. - **Optional data is common.** A student without a middle name, a report without a comment, a context variable that only some views provide: rendering nothing is usually the right output. - **Existing templates depend on it.** Many templates, some of Django's own among them, print variables that may be absent, so a loud failure mode would break them. The cost is plain: a misspelled name looks exactly like legitimately empty data. A typical gradebook case: the view passes `{"student": s, "term_report": r}`, and the template, copied from an older page, prints `{{ report.comment }}`. The teacher's comment never appears, nobody sees an error, and the bug is found weeks later by a parent asking why the comment box is always empty. Nothing in the render distinguishes "no comment was written" from "the template asked for a name the view never provided". ## How the rules differ by position | Where the invalid variable appears | What happens | |---|---| | Plain output `{{ studnet.name }}` | renders `string_if_invalid` | | Output with filters `{{ nickname\|default:"n/a" }}` | filters run only if `string_if_invalid` is `''`; otherwise the raw marker is returned and the filters are skipped | | `{% if %}`, `{% for %}`, `{% regroup %}` | the variable is treated as `None`, and filters are always applied | | A `%s` inside `string_if_invalid` | replaced with the variable's name, e.g. `MISSING: studnet.name` | So `{% if studnet.is_active %}` quietly takes the false branch, and `{% for s in studnets %}` quietly renders its `{% empty %}` clause or nothing. ## Catching typos 1. **A temporary marker.** Set `string_if_invalid` to something visible such as `"MISSING: %s"` while you debug one template, then set it back. The docs call this a debugging aid only, because Django's own templates and many third-party ones will start showing the marker. The option must be a string; any other type fails system check `templates.E002`. 2. **The `django.template` logger.** Missing context variables are logged at `DEBUG` level by the `django.template` logger, with the failing name segment and the template name. Routing that logger to the console in development surfaces every miss without changing any output. 3. **Tests that assert content.** A view test that checks the rendered response contains the student's name fails as soon as the name renders blank. This is the only one of the three that keeps working after the debugging session ends. 4. **Code review of context names.** Keeping context keys few and explicit, for example building them in one `get_context_data` method, makes a mismatched name easier to spot. ## Silent lookup is not silent everything The silence covers **failed lookups**. If a lookup reaches a callable and that code raises, the exception propagates and the render fails, unless the exception class carries `silent_variable_failure = True`. Django sets that flag on `ObjectDoesNotExist`, the base of every model's `DoesNotExist`, so a missing related object also renders as `string_if_invalid`. Any other exception, a `ZeroDivisionError` in a grade-average method for instance, surfaces as a real error. ## Example configuration for a debugging session ```python TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.request", ], "string_if_invalid": "MISSING: %s", # remove after debugging }, }, ] LOGGING = { "version": 1, "disable_existing_loggers": False, "handlers": {"console": {"class": "logging.StreamHandler"}}, "loggers": { "django.template": {"handlers": ["console"], "level": "DEBUG"}, }, } ``` With this in place, `{{ studnet.name }}` renders `MISSING: studnet.name` and the console shows the failed lookup. Remove the marker before committing, and keep the tests.

  • Why not leave string_if_invalid set to a marker in development permanently?
    Django's documentation calls it a debugging aid only. Many templates, some of Django's own among them, rely on missing variables rendering as empty, so a permanent marker shows up all over pages that are working correctly. It also changes behaviour: filters on an invalid variable are skipped when the option is non-empty, so `|default` stops working.
  • What does {% if studnet.is_active %} do when studnet is not in the context?
    Inside `{% if %}`, `{% for %}` and `{% regroup %}`, an invalid variable is treated as `None`, and `None` is falsy, so the condition is false and the `{% else %}` branch, if any, renders. There is no error and `string_if_invalid` is not used there, so a marker setting will not reveal this typo; a test or the DEBUG log will.

saying these in an interview costs you the question

  • A missing variable raises VariableDoesNotExist and returns a 500 page
  • Setting DEBUG = True makes missing template variables raise
  • string_if_invalid is safe to leave non-empty in every environment
  • |default still applies when string_if_invalid is set to a marker
  • Every exception raised during a lookup is silenced