Skip to main content

Template Resolution with Blueprints

In this project, template resolution is handled by a specialized coordination layer that allows templates to be distributed across the main application and multiple modular blueprints. This system ensures that templates can be overridden by the application while still allowing blueprints to provide their own default views.

The Dispatching Mechanism

The core of Flask's template resolution is the DispatchingJinjaLoader class found in src/flask/templating.py. Unlike a standard Jinja2 loader that might only look in a single directory, this loader acts as a proxy that delegates the search to multiple underlying loaders.

When the Jinja environment is initialized in Environment.__init__, it defaults to using a global loader created by the application:

# src/flask/templating.py

class Environment(BaseEnvironment):
def __init__(self, app: App, **options: t.Any) -> None:
if "loader" not in options:
options["loader"] = app.create_global_jinja_loader()
BaseEnvironment.__init__(self, **options)
self.app = app

The create_global_jinja_loader method in src/flask/sansio/app.py returns an instance of DispatchingJinjaLoader, which maintains a reference to the application instance to navigate its internal structure.

Search Order and Priority

The DispatchingJinjaLoader implements a strict hierarchy for template discovery. It iterates through available loaders using the _iter_loaders method:

# src/flask/templating.py

def _iter_loaders(self, template: str) -> t.Iterator[tuple[Scaffold, BaseLoader]]:
loader = self.app.jinja_loader
if loader is not None:
yield self.app, loader

for blueprint in self.app.iter_blueprints():
loader = blueprint.jinja_loader
if loader is not None:
yield blueprint, loader

This implementation reveals two critical design choices:

  1. Application Precedence: The application's own jinja_loader is always checked first. This allows developers to override any blueprint template by simply placing a file with the same name in the application's template directory.
  2. Blueprint Registration Order: Blueprints are searched in the order they were registered with the application via app.register_blueprint(). If two blueprints provide a template with the same path, the one registered earlier takes priority.

Scaffold Integration

Both the Flask application class and the Blueprint class inherit from Scaffold. The Scaffold class provides the jinja_loader property, which automatically creates a FileSystemLoader if a template_folder is defined:

# src/flask/sansio/scaffold.py

@cached_property
def jinja_loader(self) -> BaseLoader | None:
if self.template_folder is not None:
return FileSystemLoader(os.path.join(self.root_path, self.template_folder))
else:
return None

If a blueprint is initialized without a template_folder, it is effectively skipped during the template resolution process because its jinja_loader will be None.

Fast vs. Explained Resolution

The loader supports two modes of operation controlled by the EXPLAIN_TEMPLATE_LOADING configuration flag.

Fast Loading

By default, the loader uses _get_source_fast. It returns the first matching template it finds and stops searching immediately. This is optimized for production performance.

# src/flask/templating.py

def _get_source_fast(
self, environment: BaseEnvironment, template: str
) -> tuple[str, str | None, t.Callable[[], bool] | None]:
for _srcobj, loader in self._iter_loaders(template):
try:
return loader.get_source(environment, template)
except TemplateNotFound:
continue
raise TemplateNotFound(template)

Explained Loading

When app.config["EXPLAIN_TEMPLATE_LOADING"] is set to True, the loader switches to _get_source_explained. This mode iterates through all potential loaders, recording the success or failure of each attempt. This data is then passed to explain_template_loading_attempts (from flask.debughelpers) to help developers understand why a specific template was (or wasn't) loaded from a particular location.

This is particularly useful in complex applications where multiple blueprints might be contributing templates to the same namespace, making it unclear which file is actually being rendered.

Tradeoffs and Constraints

The design of DispatchingJinjaLoader prioritizes flexibility and the ability to override behavior, but it introduces certain constraints:

  • Linear Search Overhead: Because the loader performs a linear search through blueprints, applications with a very large number of blueprints might experience a slight performance hit during the initial template compilation (though Jinja's internal caching mitigates this for subsequent requests).
  • Namespace Collisions: Since all blueprints are searched for the same template name, blueprints should ideally namespace their templates (e.g., templates/my_blueprint/index.html) to avoid accidental collisions with the main app or other blueprints.
  • Static Registration: Because the search order depends on blueprint registration order, changing the order of app.register_blueprint() calls can silently change which templates are rendered if naming collisions exist.