Skip to main content

Template Engine Backends

The template engine abstraction layer provides a unified interface for interacting with different template systems. By decoupling the high-level template loading and rendering logic from the specific syntax and implementation of an engine, this project allows developers to use the Django Template Language (DTL), Jinja2, or custom backends interchangeably.

The Engine Handler

The entry point for managing template engines is the EngineHandler class, located in django/template/utils.py. This class is responsible for parsing the TEMPLATES setting and instantiating the configured backends.

In practice, the framework uses a global instance of EngineHandler accessible via django.template.engines. This handler caches engine instances to ensure they are only initialized once.

# django/template/utils.py

class EngineHandler:
def __init__(self, templates=None):
self._templates = templates
self._engines = {}

def __getitem__(self, alias):
try:
return self._engines[alias]
except KeyError:
# ... logic to find config in settings.TEMPLATES ...
params = params.copy()
backend = params.pop("BACKEND")
engine_cls = import_string(backend)
engine = engine_cls(params)

self._engines[alias] = engine
return engine

When you access engines['django'], the handler looks up the configuration for the alias "django", imports the backend class specified in the BACKEND key, and initializes it with the provided parameters.

The Base Engine Interface

All template backends must inherit from BaseEngine in django/template/backends/base.py. This class defines the contract that every engine must fulfill and provides shared logic for template discovery.

Core Methods

Subclasses are required to implement the following methods:

  • get_template(template_name): Loads a template by name and returns a backend-specific template object.
  • from_string(template_code): Creates a template object from a string of source code.

Template Discovery

BaseEngine implements a standardized way to locate template files using the DIRS and APP_DIRS settings. The template_dirs property aggregates directories from the explicit DIRS list and, if APP_DIRS is true, from the app_dirname of installed applications.

# django/template/backends/base.py

@cached_property
def template_dirs(self):
template_dirs = tuple(self.dirs)
if self.app_dirs:
template_dirs += get_app_template_dirs(self.app_dirname)
return template_dirs

The iter_template_filenames method uses these directories to generate candidate paths for a given template name, ensuring that the resulting paths are safe from directory traversal attacks via safe_join.

Django Template Language Backend

The DjangoTemplates backend in django/template/backends/django.py is the default implementation for the Django Template Language. It acts as a wrapper around the internal django.template.engine.Engine.

Key characteristics of this backend include:

  • App Directory: It sets app_dirname = "templates", meaning it looks for templates in a templates/ subdirectory within each installed app.
  • Library Management: It automatically discovers template tag libraries from installed apps via get_installed_libraries().
  • Validation: It implements a check() method to detect configuration errors, such as non-string values for string_if_invalid or duplicate template tag library names.
# django/template/backends/django.py

class DjangoTemplates(BaseEngine):
app_dirname = "templates"

def __init__(self, params):
params = params.copy()
options = params.pop("OPTIONS").copy()
# ... setup default options ...
super().__init__(params)
self.engine = Engine(self.dirs, self.app_dirs, **options)

def get_template(self, template_name):
try:
return Template(self.engine.get_template(template_name), self)
except TemplateDoesNotExist as exc:
reraise(exc, self)

Jinja2 Backend

The Jinja2 backend in django/template/backends/jinja2.py provides integration for the Jinja2 template engine.

Unlike the DTL backend, the Jinja2 backend:

  • App Directory: Sets app_dirname = "jinja2".
  • Environment Initialization: It initializes a jinja2.Environment (or a custom subclass specified in OPTIONS['environment']).
  • Context Processors: It supports Django-style context processors, which are imported and stored in template_context_processors.
# django/template/backends/jinja2.py

class Jinja2(BaseEngine):
app_dirname = "jinja2"

def __init__(self, params):
# ...
self.context_processors = options.pop("context_processors", [])
environment = options.pop("environment", "jinja2.Environment")
environment_cls = import_string(environment)

if "loader" not in options:
options["loader"] = jinja2.FileSystemLoader(self.template_dirs)

self.env = environment_cls(**options)

Direct Backend Usage

While most template interaction happens through django.template.loader.get_template, some components instantiate backends directly to create isolated environments. A primary example is found in django/forms/renderers.py, where form renderers use specific backends to load widget templates independently of the global TEMPLATES setting.

# django/forms/renderers.py

class EngineMixin:
@cached_property
def engine(self):
return self.backend(
{
"APP_DIRS": True,
"DIRS": [Path(__file__).parent / self.backend.app_dirname],
"NAME": "djangoforms",
"OPTIONS": {},
}
)

class DjangoTemplates(EngineMixin, BaseRenderer):
backend = DjangoTemplates

In this pattern, the backend class is called with a configuration dictionary identical to what would be found in settings.TEMPLATES, allowing the renderer to control exactly where its templates are loaded from.