How templates compile to Python

Jinja · Expert · 14 min

Knowing the compiled form explains performance, scoping rules and error tracebacks.

Jinja doesn't interpret templates; it compiles them to Python. The pipeline:

1. The lexer splits source into tokens, using the configured delimiters (you can change {{ and {% via Environment options). 2. The parser builds an AST of jinja2.nodes (Output, For, If, Name, Filter, ...). env.parse(source) returns it, which is what meta.find_undeclared_variables analyzes. 3. The code generator turns the AST into Python source. You can read it with env.compile(source, raw=True). 4. Python compiles that source to a code object, cached in memory and optionally by a bytecode cache.

In the generated module, the template body becomes a generator function root(context, ...) that yields strings, and each block becomes its own function in a blocks dict. That is why inheritance is cheap: the child's block functions replace the parent's when the parent's root runs. Macros become Python functions returning Markup.

Variables are resolved from the Context, a layered mapping of template variables, then globals. Lookups are cached in local Python variables, so {% set %} inside a loop creates a new local in the loop's scope, the reason assignments don't leak out of loops and blocks.

Filter and test names are resolved at compile time, so an unknown filter fails when the template is loaded, while undefined variables only matter at render time.

Tracebacks point at template lines because Jinja keeps a debug line mapping and rewrites the traceback frames.

The generated code is an implementation detail and changes between versions; inspect it to learn, never depend on it.

Example

from jinja2 import Environment
from jinja2.exceptions import TemplateAssertionError

env = Environment()
src = "{% for x in items %}{{ x|upper }}{% endfor %}"

print(env.parse(src))
# Template(body=[For(target=Name(name='x', ctx='store'), iter=Name(name='items', ...
code = env.compile(src, raw=True)
print([line for line in code.splitlines() if line.startswith("def ")])
# ['def root(context, missing=missing, environment=environment):']
print("t_1 = environment.filters['upper']" in code)          # True: resolved up front

try:
    env.from_string("{{ x|no_such_filter }}")              # fails at compile time
except TemplateAssertionError as e:
    print(e)                                                # No filter named 'no_such_filter'.
t = env.from_string("{{ x }}")
print(t.render(x=1), list(t.generate(x=2)))                 # 1 ['2']

Remember

  • Templates go lexer -> parser AST -> generated Python -> bytecode
  • Each template is a root() generator; blocks and macros become functions
  • Filters are resolved at compile time, variables at render time

Watch out for

  • Relying on the generated Python code, which is internal and changes between releases.

Check yourself

  1. When is an unknown filter name in a template detected?

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

Official docs: jinja.palletsprojects.com/en/stable/api/#low-level-api