pythonjinja2

Alternative to "with" template command for loops


I have a template file letter.txt in the format

Hello {{ first_name }} {{ last_name }}!

(In reality there are many more variables than these two, but for simplicity I'll stick with only first_name and last_name.)

This works nicely with a call like

from jinja2 import Environment, FileSystemLoader 
environment = Environment(loader=FileSystemLoader("./"))

template = environment.get_template("letter.txt")
template.render(first_name='Jane', last_name='Doe')

Now if I have a list of names that should appear, like in a form letter, I need to do the following VERY verbose with statement in the form_letter.txt template file which includes the original untouched letter.txt template.

{% for person in people %}
    {% with first_name=person.first_name, last_name=person.last_name %}
        {% include 'letter.txt' %}
    {% endwith %}
    ------8<---cut here------------
{% endfor %}

and rendering the template via:

template = environment.get_template("form_letter.txt")
template.render(people=[{'first_name': 'Jane', 'last_name': 'Doe'},
                        {'first_name': 'Bob', 'last_name': 'Builder'},
                        {'first_name': 'Sara', 'last_name': 'Sample'}])

Creating and possibly having to expand the with statement by hand is cumbersome and error prone. Is there a better way to "create a namespace inside a template" automatically from key/value of a dict like object? Like {% with **person %} or something similar?


Solution

  • Here's a slightly less verbose version, using set keyword:

    {% for person in people %}
        {% set first_name, last_name = person.values() %}
        {% include 'letter.txt' %}
        ------8<---cut here------------
    {% endfor %}
    

    You could instead refer to values directly: {% set first_name, last_name = person['first_name'], person['last_name'] %} but that does not make it much more concise than your original code.

    Another approach could be to change the data structure and infer the semantics based on the structure itself

    template = environment.get_template("form_letter.txt")
    print(template.render(people=[('Jane', 'Doe'),
                            ('Bob', 'Builder'),
                            ('Sara', 'Sample')]))
    
    {% for person in people %}
        {% set first_name, last_name = person %}
        {% include 'letter.txt' %}
        ------8<---cut here------------
    {% endfor %}
    

    The structure is trivial enough that it is obvious what each field represents and tuples guarantee order regardless of version. For more complex objects you might consider named tuples, dataclasses etc:

    from jinja2 import Environment, FileSystemLoader
    from collections import namedtuple
    environment = Environment(loader=FileSystemLoader("./"))
    
    Person = namedtuple('Person', ['first_name', 'last_name'])
    
    template = environment.get_template("form_letter.txt")
    print(template.render(people=[Person(**el) for el in [{'first_name': 'Jane', 'last_name': 'Doe'},
                            {'first_name': 'Bob', 'last_name': 'Builder'},
                            {'first_name': 'Sara', 'last_name': 'Sample'}]]))
    

    Ultimately there is no single solution that is good for all purposes.