Django template configuration

By default, Django templates are enabled on all Django projects due to the TEMPLATES variable in settings.py. Listing 3-1 illustrates the default TEMPLATES value in Django projects.

Listing 3-1. Default Django template configuration in settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

The BACKEND variable indicates the project uses Django templates. The DIRS and APP_DIRS variables tell Django where to locate Django templates and are explained in the next section. The context_processors field inside OPTIONS tells Django which context processors to enable for a Django project. In short, a context processor offers the ability to share data across all Django templates, without the need to define it in a piecemeal fashion in Django views.

Later sections in this chapter describe what data is provided by default Django context processors and how to write your own context processors to share custom data on all Django templates.

Template search paths

Django determines where to look for templates based on the values in the DIRS and APP_DIRS variables. As you can see in listing 3-1, Django defaults to an empty DIRS value and sets the APP_DIRS variable to True.

The APP_DIRS variable set to True tells Django to look for templates in Django app sub-folders named templates -- if you've never heard of the Django app concept, look over chapter 1 which describes the concept of Django apps.

The APP_DIRS behavior is helpful to contain an app's templates to an app's structure, but be aware the template search path is not aware of an app's namespace. For example, if you have two apps that both rely on a template named index.html and both app's have a method in views.py that returns control to the index.html template (e.g.render(request,'index.html')), both app's will use the index.html from the top-most declared app in INSTALLED_APPS, so only one app will use the expected index.html.

The first set of folders illustrated in listing 3-2 shows two Django apps with this type of potential template layout conflict.

Listing 3-2. Django apps with templates dirs with potential conflict and namespace qualification

# Templates directly under templates folder can cause loading conflicts
+---+-<PROJECT_DIR_project_name_conflict>
    |
    +-asgi.py
    +-__init__.py
    +-settings.py
    +-urls.py
    +-wsgi.py
    |
    +-about(app)-+
    |            +-__init__.py
    |            +-models.py
    |            +-tests.py
    |            +-views.py
    |            +-templates-+
    |                        |
    |                        +-index.html
    +-stores(app)-+
                 +-__init__.py
                 +-models.py
                 +-tests.py
                 +-views.py
                 +-templates-+
                             |
                             +-index.html


# Templates classified with additional namespace avoid loading conflicts
+---+-<PROJECT_DIR_project_name_namespace>
    |
    +-asgi.py
    +-__init__.py
    +-settings.py
    +-urls.py
    +-wsgi.py
    |
    +-about(app)-+
    |            +-__init__.py
    |            +-models.py
    |            +-tests.py
    |            +-views.py
    |            +-templates-+
    |                        |
    |                        +-about-+
    |                                |
    |                                +-index.html
    +-stores(app)-+
                 +-__init__.py
                 +-models.py
                 +-tests.py
                 +-views.py
                 +-templates-+
                             |
                             +-stores-+
                                      |
                                      +-index.html

To fix this potential template search conflict, the recommended practice is to add an additional sub-folder to act as a namespace inside each templates directory, as illustrated in the second set of folders in listing 3-2.

In this manner, you can re-direct control to a template using this additional namespace sub-folder to avoid any ambiguity. So to send control to the about/index.html template you would declare render(request,'about/index.html') and to send control to the stores/index.html you would declare render(request,'stores/index.html').

If you wish to disallow this behavior of allowing templates to be loaded from these internal app sub-folders, you can do so by setting APP_DIRS to FALSE.

A more common approach to define Django templates is to have a single folder or various folders that live outside app structures to hold Django templates. In order for Django to find such templates you use the DIRS variable as illustrated in listing 3-3.

Listing 3-3. DIRS definition with relative path in settings.py

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
PROJECT_DIR = Path(__file__).resolve().parent

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [ PROJECT_DIR / 'templates',
                 PROJECT_DIR / 'dev_templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

As you can see in listing 3-3, you can declare various directories inside the DIRS variable. Django looks for templates inside DIRS values and then in templates folder in apps -- if APP_DIRS is True -- until it either finds a matching template or throws a TemplateDoesNotExist error.

Also note the DIRS values in listing 3-3 relies on a path determined dynamically by the PROJECT_DIR variable. This approach is helpful when you deploy a Django project across different machines, because the path is relative to the top-level Django project directory (i.e. where the settings.py and main urls.py file are) and adjusts dynamically irrespective of where a Django project is installed (e.g. /var/www/, /opt/website/, C://website/).

Invalid template variables

By default, Django templates do not throw an error when they contain invalid variables. This is due to design choices associated with the Django admin which also uses Django templates.

While this is not a major issue in most cases, it can be frustrating for debugging tasks as Django doesn't inform you of misspelled or undefined variables. For example, you could type {{datee}} instead of {{date}} and Django ignores this by outputting an empty string '', you could also forget to pass a variable value to a template in the view method and Django also silently outputs an empty string '' even though you may have it defined in the template.

To enable Django to inform you when it encounters an invalid variable in Django templates, you can use the string_if_invalid option. The first configuration option for string_if_invalid shown in listing 3-4 outputs a visible string instead of an empty string ''.

Listing 3-4 Output warning message for invalid template variables with string_if_invalid

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
PROJECT_DIR = Path(__file__).resolve().parent

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [ PROJECT_DIR / 'templates',
                 PROJECT_DIR / 'dev_templates']
        'APP_DIRS': True,
        'OPTIONS': {
            'string_if_invalid': "**** WARNING INVALID VARIABLE %s ****",
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

As you can see in listing 3-4, string_if_invalid is assigned the string "**** WARNING INVALID VARIABLE %s ****". When Django encounters an invalid variable, it replaces the occurrence with this string, where the %s variable gets substituted with the invalid variable name, allowing you to easily locate where and what variables are invalid.

Another configuration option for the string_if_invalid option is to perform more complex logic when an invalid variable is encountered. For example, listing 3-5 illustrates how you can raise an error so the template fails to render in case an invalid variable is found.

Listing 3-5. Error generation for invalid template variables with string_if_invalid

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
PROJECT_DIR = Path(__file__).resolve().parent

class InvalidTemplateVariable(str):
    def __mod__(self,other):
        from django.template.base import TemplateSyntaxError
        raise TemplateSyntaxError("Invalid variable : '%s'" % other)

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [ PROJECT_DIR / 'templates',
                 PROJECT_DIR / 'dev_templates']
        'APP_DIRS': True,
        'OPTIONS': {
            'string_if_invalid': InvalidTemplateVariable("%s"),
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

In listing 3-5 string_if_invalid is assigned the InvalidTemplateVariable class that uses the %s input variable, which represents the invalid variable name -- just like the previous example in listing 3-4.

The InvalidTemplateVariable class is interesting because it inherits its behavior from the str(string) class and uses a Python __mod__ (modulo) magic method implementation. While the __mod__ (modulo) magic method is proper of number operations, in this case, it's useful because the passed in string uses the % (modulo) symbol, which makes the __mod__ method run. Inside the __mod__ method we just raise the TemplateSyntaxError error with the invalid variable name to halt the execution of the template.

Caution The Django admin might get mangled or broken with a custom string_if_invalid value.The Django admin templates in particular rely heavily on the default string_if_invalid outputting empty strings '', due to the level of complexity in certain displays. In fact, this string_if_invalid default behavior is often considered a 'feature', as much as it's considered a 'bug' or 'annoyance'.Therefore if you use one of the approaches in listing 3-4 or listing 3-5 to override string_if_invalid, be aware you will most likely mangle or brake Django admin pages. If you rely on the Django admin, you should only use these techniques to debug a project's templates.

Debug output

When you run a Django project with the top-level DEBUG=True setting and an error occurs, Django templates output a very detailed page to make the debugging process easier -- see the chapter Django application management for more details on the DEBUG variable, specifically the section Django settings.py for the real world.

By default, Django templates reuse the top-level DEBUG variable value to configure template debug activity. Behind the scenes, this configuration is set through the debug field inside OPTIONS of the TEMPLATES variable. Figure 3-1 illustrates what an error page looks when DEBUG=True.

Figure 3-1. Django error page when DEBUG=True automatically sets template OPTION to 'debug':True

As you can see in figure 3-1, Django prints the location of the template, as well as a snippet of the template itself to make it easier to locate an error. This template information is generated due to the 'debug':True option, which is set based on the top-level DEBUG variable. However, you can explicitly set the debug option to False as illustrated in listing 3-6, in which case the error page would result without any template details and just the traceback information, as illustrated in Figure 3-2.

Listing 3-6.Option with debug equals False omits template details

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
PROJECT_DIR = Path(__file__).resolve().parent

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [ PROJECT_DIR / 'templates',
                 PROJECT_DIR / 'dev_templates']
        'APP_DIRS': True,
        'OPTIONS': {
            'debug':False,
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Figure 3-2. Django error page when DEBUG=True and explicit OPTION 'debug':False

Auto-escape

By default, Django templates use the security practice to auto-escape certain characters -- described in table 3-1 -- contained in dynamically generated constructs (e.g. variables, tags, filters). Auto-escaping converts characters that can potentially mangle a user interface or produce dangerous outcomes into safe representations, a process that was described in the first section of this chapter.

However, you can globally disable auto-escaping on all Django templates -- and knowingly render < as <, > as >, etc... -- with the autoescape field in OPTIONS as shown in listing 3-7.

Listing 3-7 Option with autoescape equals False omits autoescaping on all Django templates

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
PROJECT_DIR = Path(__file__).resolve().parent

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [ PROJECT_DIR / 'templates',
                 PROJECT_DIR / 'dev_templates']
        'APP_DIRS': True,
        'OPTIONS': {
            'autoescape':False,
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

It's worth mentioning that an alternative to disabling auto-escaping on every Django template -- as it's done in listing 3-7 -- is to selectively disable auto-escaping. You can either use the {% autoescape off %} tag to disable auto-escaping on a section of a Django template or the safe filter to disable auto-escaping on a single Django template variable.

If you do decide to disable auto-escaping on all Django templates as illustrated in listing 3-7 -- which I frankly wouldn't recommend if you plan to use just HTML, because of the potential security risk -- you can also granularly enable auto-escaping again if required. You can either use the {% autoescape on %} tag to enable auto-escaping on a section of a Django template or the escape filter to escape a single Django template variable.

File charset

Files used in Python projects often declare an encoding value at the top (e.g. # -*- coding: utf-8 -*-) based on the Python PEP-263 specification[2], which ensures the characters in the file are interpreted correctly. In Django templates, you don't define the underlying file's encoding in this manner, but instead do it inside a project's settings.py file.

There are two ways to declare the encoding character for Django templates: explicitly as part of the file_charset field in OPTIONS inside the TEMPLATES variable or via the top level FILE_CHARSET variable in settings.py. The explicit declaration in file_charset within OPTIONS takes precedence over the FILE_CHARSET assignment, but the value of file_charset defaults to FILE_CHARSET which in itself defaults to utf-8 (Unicode) encoding.

So by default, Django template encoding is assigned to utf-8 or Unicode which is one of the most widely used encodings in software. Nevertheless, in the event you decide to incorporate data into Django templates that isn't utf-8 compatible (e.g. Spanish vowels with accents like á or é encoded as ISO-8859-1 or Kanji characters like 漢 or 字 encoded as JIS) you must define the FILE_CHARSET value in a project's settings.py file -- or directly in the file_charset field in OPTIONS inside the TEMPLATES -- so Django template data is interpreted correctly.

Django templates can be assigned any encoding value from Python's standard encoding values[3].

Automatic access to custom template tag/filter modules

Django templates have access to a series of built-in tags & filters that don't require any setup steps. However, if you plan to use a third-party template tag/filter module or write your own template tag/filter module, then you need to setup access on each Django template with the {% load %} tag (e.g. {% load really_useful_tags_and_filters %}), a process that can get tiresome if you need to access a particular tag/filter on dozens or hundreds of templates.

To automatically gain access to third-party template tags/filters or your own template tags/filters as if they were built-in tags/filters (i.e. without requiring the {% load %} tag), you can use the builtins field in OPTIONS as illustrated in listing 3-8.

Listing 3-8 Option with builtins to gain automatic access to tags/filters on all templates

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
PROJECT_DIR = Path(__file__).resolve().parent

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [ PROJECT_DIR / 'templates',
                 PROJECT_DIR / 'dev_templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
            'builtins': [
                 'coffeehouse.builtins',
                 'thirdpartyapp.customtags.really_useful_tags_and_filters',
            ],
        },
    },
]

As you can see in listing 3-8, the builtins field accepts a list of modules that includes tags/filters for built-in treatment. In this case, coffeehouse.builtins represents a builtins.py file -- that includes the custom tags/filters -- under a project named coffeehouse. And the thirdpartyapp.customtags.really_useful_tags_and_filters is a third-party package with tags/filters that we also want to access in Django templates without the need to use the {% load %} tag.

Another default behavior of third-party template tag/filter modules and custom template tag/filter modules is they require to use their original label/name for reference, while the latter also requires to be placed inside a folder named templatetags in a registered Django app. These two default behaviors can be overridden with the libraries field in OPTIONS as illustrated in listing 3-9.

Listing 3-9 Option with libraries to register tags/filters with alternative label/name and under any project directory.

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
PROJECT_DIR = Path(__file__).resolve().parent

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [ PROJECT_DIR / 'templates',
                 PROJECT_DIR / 'dev_templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
            'libraries': {
                 'coffeehouse_tags': 'coffeehouse.tags_filters.common',
            },
        },
    },
]

The libraries statement in listing 3-9 'coffeehouse_tags': 'coffeehouse.tags_filters.common' tells Django to load the common.py file -- that includes the custom tags/filters -- from the tags_filters folder in the coffeehouse project and make it accessible to templates through the coffeehouse_tags reference (e.g. {% load coffeehouse_tags %}). With the approach in listing 3-9, you can place custom tag/filter modules anywhere in a Django project, as well as assign custom tag/filter modules -- or third-party tag/filter modules -- an alternative reference value instead of their original label/name.

Template loaders

Earlier in the section 'Template search paths', I described how Django searches for templates using the DIRS and APP_DIRS variables which are part of Django's template configuration. However, I intentionally omitted a deeper aspect associated with this template search process: each search mechanism is backed by a template loader.

A template loader is a Python class that implements the actual logic required to search & load templates. Table 3-2 illustrates the built-in template loaders available in Django.

Table 3-2. Built-in Django template loaders

Template loader class Description
django.template.loaders.app_directories.Loader Searches and loads templates from sub-directories named templates in all apps declared in INSTALLED_APPS. Enabled by default when APP_DIRS is True.
django.template.loaders.cached.Loader Searches for templates from an in-memory cache, after loading templates from a file-system or app directory loader.
django.template.loaders.filesystem.Loader Searches and loads templates in directories declared in the DIRS variable. Enabled by default when DIRS is not empty.
django.template.loaders.locmem.Loader Searches for templates from an in-memory cache, after loading templates from a Python dictionary.

The Django template loaders that are automatically enabled are dependent on configuration parameters. By default, the django.template.loaders.filesystem.Loader template loader is always available, if 'APP_DIRS': True is set then the django.template.loaders.app_directories.Loader template loader is also enabled, and if DEBUG == False then the django.template.loaders.cached.Loader template loader is also enabled.

Nevertheless, it's possible to override this default behavior by explicitly specifying a list of template loaders using the loaders field in OPTIONS inside TEMPLATES.

  1. https://www.python.org/dev/peps/pep-0263/     

  2. https://docs.python.org/3/library/codecs.html#standard-encodings