Writing extensions and async rendering

Jinja · Expert · 15 min

Extensions let you add syntax such as caching or i18n blocks when filters and macros aren't enough.

Filters, tests and globals extend what expressions can do. Extensions go further and add new tags. Built-in ones are enabled by name:

Environment(extensions=["jinja2.ext.i18n", "jinja2.ext.loopcontrols", "jinja2.ext.do"])

i18n adds {% trans %} blocks for gettext, loopcontrols adds break and continue, do runs an expression without output, and debug adds {% debug %} to dump the context.

A custom extension subclasses jinja2.ext.Extension, declares tags = {"mytag"} and implements parse(self, parser). The parser hands you the token stream; you consume your tag, parse arguments with parser.parse_expression(), collect the body with parser.parse_statements(("name:endmytag",), drop_needle=True), and return AST nodes. The most common pattern returns a nodes.CallBlock that calls a method on the extension via self.call_method("_render", args); that method receives caller, a function that renders the body. This is how fragment caching extensions work: compute a key, return cached output or call caller() and store it.

Extensions can also add Environment attributes with self.environment.extend(...) and preprocess source or filter the token stream.

Async rendering. Environment(enable_async=True) compiles templates so they can await async functions and iterate async generators passed in the context. Render with await template.render_async(...). It is useful when templates pull data from async sources, but it is slower than sync rendering, so prefer loading data first and rendering synchronously.

Example

import asyncio
from jinja2 import Environment, nodes
from jinja2.ext import Extension

class ShoutExtension(Extension):
    tags = {"shout"}

    def parse(self, parser):
        lineno = next(parser.stream).lineno          # consume the "shout" token
        times = parser.parse_expression()            # {% shout 2 %}
        body = parser.parse_statements(("name:endshout",), drop_needle=True)
        return nodes.CallBlock(self.call_method("_render", [times]), [], [], body).set_lineno(lineno)
    def _render(self, times, caller):
        return (caller().strip().upper() + "! ") * times
env = Environment(extensions=[ShoutExtension, "jinja2.ext.loopcontrols"])
print(env.from_string("{% shout 2 %}hi {{ name }}{% endshout %}").render(name="ann"))
# HI ANN! HI ANN!
print(env.from_string("{% for i in range(9) %}{% if i > 2 %}{% break %}{% endif %}{{ i }}{% endfor %}").render())
# 012

async def get_user():
    return "Bo"
aenv = Environment(enable_async=True)
print(asyncio.run(aenv.from_string("Hello {{ get_user() }}").render_async(get_user=get_user)))
# Hello Bo

Remember

  • Built-in extensions: i18n, loopcontrols, do, debug
  • A custom tag = Extension subclass with tags and parse() returning AST nodes
  • CallBlock + call_method lets Python code wrap a rendered body
  • enable_async + render_async await async functions in templates

Watch out for

  • Writing an extension for something a filter, global or macro could do more simply.

Check yourself

  1. In an extension's _render(self, ..., caller) method, what does caller() return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: jinja.palletsprojects.com/en/stable/extensions/#writing-extensions