How to Make Django Raise an Error for Missing Template Variables

It’s all too easy to forget to pass a variable to your template or make a typo in a variable name. Unfortunately, it can be quite hard to debug such mistakes, since Django’s default behaviour is to ignore the problem and render an empty string.
In this post we’ll look at several techniques to check for missing template variables:
- Using the
string_if_invalid
option - the built-in option, but a bit cumbersome. - With a logging filter that raises exceptions - this is my preferred technique. But it could break templates in your project that rely on the default missing variable beahviour.
- with a logging filter that promotes messages to errors - you can use this to phase in the above exception-raising behaviour.
- With Jinja’s
StrictUndefined
- if you can switch your templates to render with Jinja, you can enable this option, as well as gain some other advantages.
Alright, there’s plenty of soil to till, so let’s dig in!
With the string_if_invalid
option
The DTL has an option called string_if_invalid
. Templates render this for missing variables, and it defaults to the empty string. The docs have more detail on how this works.
You can configure it in your settings like so (but don’t copy this example):
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"string_if_invalid": "😱 MISSING VARIABLE %s 😱",
},
}
]
Using some emoji and shouty upper case will help the output stand out when manually testing pages. Django will replace the %s
with the name of the missing variable, which can help track down the problem.
Setting the option unconditionally like this is not recommended, since it can appear in production as well. Instead, you generally want to set the option during development and tests, for example:
import sys
testing = sys.argv[1:2] == ["test"]
...
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"string_if_invalid": (
"😱 MISSING VARIABLE %s 😱" if DEBUG or testing else ""
),
},
}
]
The testing
variable here checks if the running command is test
, so it’s looking for Django’s testing framework. If you’re using pytest you can instead use:
testing = "pytest" in sys.modules
There are many other ways to handle settings though, such as overriding some settings during tests - adapt as required.
With the option set, you can see it in action by loading a page with a bad variable. For example, take this index view:
from django.shortcuts import render
def index(request):
return render(request, "index.html", {"nome": "Adam"})
nome
is a typo of name
, which is what the template is looking for:
<p>Hi {{ name }}!</p>
When loading the page in development you’d see:
Hi 😱 MISSING VARIABLE name 😱!
In theory, you can then spot this when checking your pages. You might miss it though, or the missing variables might be in non-visible parts of the page, such as HTML attributes like <a href="{{ url }}>
. It’s thus useful to check for string_if_invalid
in tests…
Checking in Tests
In tests, you can check for missing variables by searching for your string_if_invalid
value (without the %s
). You could do this in every test with a string search, like:
from http import HTTPStatus
from django.test import TestCase
class IndexTests(TestCase):
def test_success(self):
response = self.client.get("/")
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertNotIn("😱 MISSING VARIABLE", response.content.decode())
With the above typo, this fails like:
$ ./manage.py test
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_success (example.core.tests.test_views.IndexTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/.../example/core/tests/test_views.py", line 11, in test_success
self.assertNotIn("😱 MISSING VARIABLE", response.content.decode())
AssertionError: '😱 MISSING VARIABLE' unexpectedly found in '<p>Hi 😱 MISSING VARIABLE name 😱!</p>\n'
----------------------------------------------------------------------
Ran 1 test in 0.014s
FAILED (failures=1)
Destroying test database for alias 'default'...
Okay, that’s usable. But it would be annoying to have to add the assertion to every test that renders a template.
You can make this check always run by creating a custom test client subclass that checks for every response. Then, you can create custom test case classes that use your custom client class via the client_class
attribute:
import re
from django import test
class CustomClient(test.Client):
def request(self, *args, **kwargs):
response = super().request(*args, **kwargs)
# Check for missing template variables
if not response.streaming:
missing_vars = re.findall(
r"😱 MISSING VARIABLE (.*?) 😱".encode(), response.content
)
if missing_vars:
raise AssertionError(
"Missing template variables: "
+ ",".join(v.decode() for v in missing_vars)
)
return response
class TestCaseMixin:
client_class = CustomClient
class SimpleTestCase(TestCaseMixin, test.SimpleTestCase):
pass
class TestCase(TestCaseMixin, test.TestCase):
pass
class TransactionTestCase(TestCaseMixin, test.TransactionTestCase):
pass
Your project can then use your custom test case classes everywhere:
from http import HTTPStatus
from example.test import TestCase
class IndexTests(TestCase):
def test_success(self):
response = self.client.get("/")
self.assertEqual(response.status_code, HTTPStatus.OK)
Running this test now fails with a nice clear message listing all missing variables:
$ ./manage.py test
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_success (example.core.tests.test_views.IndexTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/.../example/core/tests/test_views.py", line 8, in test_success
response = self.client.get("/")
File "/.../venv/lib/python3.10/site-packages/django/test/client.py", line 836, in get
response = super().get(path, data=data, secure=secure, **extra)
File "/.../venv/lib/python3.10/site-packages/django/test/client.py", line 424, in get
return self.generic(
File "/.../venv/lib/python3.10/site-packages/django/test/client.py", line 541, in generic
return self.request(**r)
File "/.../example/test.py", line 16, in request
raise AssertionError(
AssertionError: Missing template variables: name
----------------------------------------------------------------------
Ran 1 test in 0.014s
FAILED (failures=1)
Destroying test database for alias 'default'...
Fan-tabby-tastic!
This approach will cover tests for views, but it won’t cover other situations where you might render templates, for example for custom widgets, or emails. Adapt the check to include it for other rendering paths as appropriate.
With a logging filter that raises exceptions
The string_if_invalid
option is simple and sometimes sufficient, but it has many drawbacks:
- You might miss it when manually testing pages, especially for variables affecting non-visible parts of the page like HTML attributes.
- You need to add checks in unit tests.
- You can’t enable it in production, at least without showing messages to users.
- The output can’t tell you which templates are missing variables. To debug an error you might have to follow many
{% extends %}
and{% include %}
tags. - Some templates in third-party packages, and even those in the admin, depend on missing variables rendering as the empty string. Enabling the option can mess up their display.
A second option that I’ve used is to hook into the DTL’s “missing variable” logging. The django.template
logger logs debug-level messages for each missing variable. With a custom logging filter, we can act on those log messages, and even transform them into exceptions.
You can add a custom logging filter to the logger in the LOGGING
setting, as per Django’s configring logging docs. For example:
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {
"missing_variable_error": {
"()": "example.logging.MissingVariableErrorFilter",
},
},
"loggers": {
"django.template": {
"level": "DEBUG",
"filters": ["missing_variable_error"],
},
},
}
This sets the level for the django.template
logger to DEBUG
, and adds a custom filter example.logging.MissingVariableErrorFilter
. Here’s an example filter class which you might want to adapt:
import logging
class MissingVariableError(Exception):
"""
A variable was missing from a template. Used as an alternative to
django.template.base.VariableDoesNotExist, because that exception has some
meaning within the template engine.
"""
class MissingVariableErrorFilter(logging.Filter):
"""
Take log messages from Django for missing template variables and turn them
into exceptions.
"""
ignored_prefixes = (
"admin/",
"auth/",
"debug_toolbar/",
"django/",
"wagtail/",
"wagtailadmin/",
"wagtailblog/",
"wagtailembeds/",
"wagtailimages/",
"wagtailsites/",
"wagtailusers/",
)
def filter(self, record):
if record.msg.startswith("Exception while resolving variable "):
variable_name, template_name = record.args
if not template_name.startswith(self.ignored_prefixes):
raise MissingVariableError(
f"{variable_name!r} missing in {template_name!r}"
) from None
return False
Let’s deconstruct this code like cheesecake at a fancy restaurant…
A logging filter class is a way of adding logic to hide some log messages. It needs only one method,
filter(record)
, that should returnTrue
if the log message is allowed, orFalse
if not.This logging filter checks from the exact message that Django logs by checking the message with
startswith()
. This message is from within Django’sVariable
class (source).Using string matching like this is a bit fragile, as it can break if Django changes the message, but it’s the best we can do. At least the message hasn’t changed in the seven years since it was added in this commit.
For matching messages, the two arguments are unpacked into the
variable_name
andtemplate_name
variables.Some templates are ignored, based on their prefix (essentially the template system directory they live in). When using this filter in practice, I’ve found that many templates in contrib and third-party packages depend on the default missing variable behaviour. For such templates, there’s no utility in raising an exception, so they need to be ignored.
The list of prefixes in
ignored_prefixes
is based on some common packages used in a real-world project:admin/
fordjango.contrib.admin
auth/
fordjango.contrib.auth
debug_toolbar/
for django-debug-toolbardjango/
for core Django templates used in form rendering- All
wagtail*
prefixes for Wagtail and its extensions.
You will probably need to add more depending on the packages you use.
Finally, the code raises the
MissingVariableError
exception, which is a custom exception class living in this file. Django has a similarVariableDoesNotExist
, but you cannot use it here, as Django will catch it.The code raises the exception
from None
, to disable exception chaining. The new exception is thus not associated with thetry / except
that wraps the logging call, and Python doesn’t report unnecessary detail.
Allllrighty then. When you hit a template with a missing variable in debug mode, it looks like this:

Brilliant. Django even shows you where in the template the problem lies.
To check the filter is hooked up correctly, and that it continues to work in the future, add some delicious tests:
from django.template import Context, Template
from django.test import SimpleTestCase
from example.logging import MissingVariableError
class MissingVariableErrorFilterTests(SimpleTestCase):
def test_missing_variable(self):
template = Template("Hi {{ name }}", name="index.html")
context = Context({"nome": "Adam"})
with self.assertRaises(MissingVariableError) as cm:
template.render(context)
self.assertEqual(str(cm.exception), "'name' missing in 'index.html'")
def test_ignored_prefix(self):
template = Template("Hi {{ name }}", name="admin/index.html")
context = Context({"nome": "Adam"})
result = template.render(context)
self.assertEqual(result, "Hi ")
Abracadabra:
$ ./manage.py test example.test_logging
Found 2 test(s).
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.004s
OK
Yeet that into production and deal with the consequences… or, use the next technique…
With a logging filter that promotes messages to errors
The above approach may be a bit harsh for existing projects. You may have many templates that depend on the default missing variable behaviour. Or you might not have sufficient test coverage to ensure that you can fix all missing variables if they raise exceptions.
Instead of raising exceptions, you can modify the logging to instead use the “error” level to make missing variables obvious. The default “debug” level likely doesn’t show up in any of your monitoring software, but error records should. For example, the default configuration for Sentry captures all logged errors.
Here’s a version of the template filter that doesn’t raise exceptions, and instead changes non-ignored records to use the ERROR
level:
import logging
class MissingVariableErrorFilter(logging.Filter):
"""
Take log messages from Django for missing template variables and turn them
into exceptions.
"""
ignored_prefixes = (
"admin/",
"auth/",
"debug_toolbar/",
"django/",
"wagtail/",
"wagtailadmin/",
"wagtailblog/",
"wagtailembeds/",
"wagtailimages/",
"wagtailsites/",
"wagtailusers/",
)
def filter(self, record):
if record.msg.startswith("Exception while resolving variable "):
variable_name, template_name = record.args
if not template_name.startswith(self.ignored_prefixes):
record.level = logging.ERROR
return True
return False
…and corresponding tests:
import logging
from django.template import Context, Template
from django.test import SimpleTestCase
class MissingVariableErrorFilterTests(SimpleTestCase):
def test_missing_variable(self):
template = Template("Hi {{ name }}", name="index.html")
context = Context({"nome": "Adam"})
with self.assertLogs("django.template", logging.DEBUG) as cm:
result = template.render(context)
self.assertEqual(result, "Hi ")
self.assertEqual(len(cm.records), 1)
self.assertEqual(cm.records[0].level, logging.ERROR)
self.assertEqual(
cm.records[0].getMessage(),
"Exception while resolving variable 'name' in template 'index.html'.",
)
def test_ignored_prefix(self):
template = Template("Hi {{ name }}", name="admin/index.html")
context = Context({"nome": "Adam"})
with self.assertNoLogs("django.template", logging.DEBUG):
result = template.render(context)
self.assertEqual(result, "Hi ")
Nice one.
You could deploy this, and then fix missing variables as they’re discovered. Then, when you’re confident enough, switch to the exception-raising version as above. On very large projects you could even combine the approaches, and raise exceptions for a list of prefixes that you gradually expand, iteratively making your templates stricter.
With Jinja’s StrictUndefined
Django has built-in support to use Jinja as an alternative template engine. This can provide a pretty meaningful performance boost, more flexible syntax, and some nice features like macros. It also has more options for handling missing variables.
Jinja turns missing variables into instances of the Jinja environment’s undefined type. The behaviour of this class then affects what happens for use of the variable. There several undefined types built-in to Jinja - here are the most relevant:
Undefined
, the default, acts like the DTL default. When printed, it returns the empty string.DebugUndefined
is a bit like setting the DTLstring_if_invalid
option, as above. When printed, it displays the tag for the missing variable, e.g.{{ name }}
.StrictUndefined
raises anUndefinedError
exception on printing or any other operation. This is like our above DTL logging filter that raises exceptions.
By default Django’s Jinja backend sets the undefined type to DebugUndefined
when settings.DEBUG
is enabled, and Undefined
otherwise. This gives you a little extra help for finding missing variables in development, without breaking your design in production.
But, we’re here to make things stricter! We can do so with… StrictUndefined
.
You can set this in your settings:
from jinja2 import StrictUndefined
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
# ...
},
{
"BACKEND": "django.template.backends.jinja2.Jinja2",
"DIRS": [BASE_DIR / "templates"],
"OPTIONS": {
"undefined": StrictUndefined,
},
},
]
Note that Jinja is the second template engine here. For most projects, you need the DTL as your default engine, since Django’s admin and many third-party packages depend on it.
After setting undefined
, every missing variable error will raise UndefinedError
:

Jinja stack traces show the responsible line in the template, so here the bottom of the stack trace shows:
/.../templates/index.html, line 1, in top-level template code
<p>Hi {{ name }}!</p>
Slick!
Fin
That’s it for our grand tour of missing variable techniques. I hope you can use one to make your templates easier to work with.
😱 MISSING VARIABLE witty_sign_off 😱
—Adam
Improve your Django develompent experience with my new book.
One summary email a week, no spam, I pinky promise.
Related posts:
- How to Add a Favicon to Your Django Site
- You Probably Don’t Need Django’s
get_user_model()
- How to Set Up Source Maps with Django
Tags: django