Skip to content

Instantly share code, notes, and snippets.

@MatMoore
Last active April 10, 2025 11:18
Show Gist options
  • Save MatMoore/a109d4496bdf0ecddd1a40c387629924 to your computer and use it in GitHub Desktop.
Save MatMoore/a109d4496bdf0ecddd1a40c387629924 to your computer and use it in GitHub Desktop.
Nunjucks / Jinja compatability spike

Nunjucks / Jinja compatibility spike

Purpose of spike

Nunjucks templates are almost (but not quite) compatible with Jinja.

We wanted to find out if we could adapt existing templates written for Nunjucks so that they would render in Jinja.

Specifically, we wanted to understand what would be involved in making nhs-frontend Nunjucks templates cross-compatible with Jinja.

Conclusion

Most changes are minor

There are a number of small differences between Nunjucks 3 and Jinja that you need to be aware of if you want to support both.

Most of these are fairly minor changes to the templates.

In our whole design system, we found one small snippet we couldn't easily rewrite to be cross-compatible:

{% set isConditional = false %}
{% for item in params.items %}
  {% if item.conditional.html %}
    {% set isConditional = true %}
  {% endif %}
{% endfor %}

This is due to differences in scoping rules, detailed below.

Currently, we need to add a custom filter to be able to handle this in both template engines (but we're still looking for another way)

We have a proof of concept here: nhsuk/nhsuk-frontend#1199

installJinjaCompat does not help

Nunjucks has experimental support for making templates more Jinja compatible, installJinjaCompat.

We found this did not help us. Consider the case where you have a value like this:

{
  "items": []
}

In Nunjucks, you can call either var.items or var["items"] to retrieve the array. In Jinja, a mapping is typically a python dictionary, so var.items returns the dictionary's items() method. To be cross-compatible, our template needs to use var["items"].

This works because in Jinja/Python, objects have two ways of attaching values to objects: x.a and x['a'] are different values. Jinja variable access considers both, with the dictionary lookup taking precedence when you use the square bracket syntax.

installJinjaCompat changes variable lookup so that all items lookup return a fake items() method. This means there is no way to access the actual value in the object, so we would have to drop support for any objects with items keys (or any other keys that clash with python dictionary methods).

Nunjucks 4 will make it easier to support both template engines

There is ongoing work on Nunjucks 4, which drastically improves the compatibility with Jinja. However, this is a way off yet. See Nunjucks 4 roadmap and release requirements.

Nunjucks 4 will add the following

  • Missing core filters from Jinja
  • self variable
  • break and continue in loops
  • recursive for loops
  • {% block scoped %}
  • make empty variables falsey
  • fixes to {% call %} and caller

List of differences between Nunjucks 3 and Jinja

Attributes may clash with dictionary methods

As described above, you need to be careful when using dotted variable lookups.

For mappings, avoid python dictionary methods:

  • clear
  • copy
  • get
  • items
  • keys
  • pop
  • popitems
  • setdefault
  • update
  • values

The solution is to use square bracket syntax instead.

Bad (clashes with dict.items() in Jinja):

{% for item in params.items %}

Good:

{% set items = params['items'] if 'items' in params else [] %}

{% for item in items %}

String concatenation (+ vs ~)

Jinja's + operator does not coerce its arguments to strings. Nunjucks does a lot of coercing to string (following Javascript rules.)

To concatenate a string you should use the (undocumented in Nunjucks) ~ operator in place of +.

Mapping literals

Mapping literals in Jinja need to have their keys quoted. If you forget to do this, the keys will be interpreted as variables when rendering in Jinja and will evaluate to undefined.

Bad:

{{ hint({
  id: itemHintId,
  classes: 'nhsuk-checkboxes__hint',
  attributes: item.hint.attributes,
  html: item.hint.html,
  text: item.hint.text
}) }}

Good:

{{ hint({
  "id": itemHintId,
  "classes": 'nhsuk-checkboxes__hint',
  "attributes": item.hint.attributes,
  "html": item.hint.html,
  "text": item.hint.text
}) }}

Scoping differences

In Jinja you cannot modify a variable in a loop and reference it outside the loop, due to its scoping behaviour.

Bad:

{% set isConditional = false %}
{% for item in items %}
  {% if item.conditional %}
    {% set isConditional = true %}
  {% endif %}
{% endfor %}

On the other hand, in Nunjucks, you can't modify attributes of objects in set statements.

Also bad:

{% set ns.isConditional = false %}
{% for item in items %}
  {% if item.conditional %}
    {% set ns.isConditional = true %}
  {% endif %}
{% endfor %}

In some cases, this can be avoided by using filters instead of a loop.

Good:

{% set isConditional = items | selectattr("conditional") | list | length > 0 %}

Missing filters

Nunjucks 3 does not support all of the filters in Jinja, and adds some of its own. To support both, you need to stick to the common subset.

In practice, this is not a major issue, except that map(), if available, could be used to replace problematic for loops that don't function in Jinja (see the comments on scoping above).

The below list may not be 100% accurate, as not everything is documented in Nunjucks.

Filter Jinja Nunjucks
abs()
attr()
batch()
capitalize()
center()
default()
dictsort()
dump()
escape()
filesizeformat()
first()
float()
forceescape()
format()
groupby()
indent()
int()
items()
join()
last()
length()
list()
lower()
nl2br()
map()
max()
min()
random()
reject()
rejectattr()
replace()
round()
safe()
selectattr() only single-argument form
slice()
sort()
string()
striptags()
sum()
title() ✅  
trim()
tojson()
truncate()
upper()
urlize()
urlencode()
wordcount()
wordwrap()
xmlattr()

Javascript methods

JS methods like includes cannot be used in Jinja.

Instead of includes use the in keyword. Instead of length use the length filter.

Bad:

{{ items.length }}

Good:

{{ items | length }}

Strict equality operators

Strict equality operators (=== and !===), do not exist in Jinja. The is keyword (e.g. foo is True), works in Jinja but does not work in Nunjucks.

If the type of a value is unknown, == true can be safely used in place of === true. This expression is true for a boolean true value or the number 1, and false for any other values, including strings.

== false is weaker than === false and should be avoided. It is true for false, null, and undefined values, but values of 0, "", [] or {} will yield different results depending on whether you are using Nunjucks or Jinja.

Behaviour of undefined values

In Jinja the behaviour of undefined depends on configuration. For compatibility with Nunjucks, configured undefined to be ChainableUndefined. For example, in Flask:

from jinja2 import ChainableUndefined, ChoiceLoader, FileSystemLoader

app.jinja_options = {
    "undefined": ChainableUndefined,  # This is needed to prevent jinja from throwing an error when chained parameters are undefined
    "loader": ChoiceLoader(
        [FileSystemLoader(ROOT / "packages"), FileSystemLoader(ROOT / "app")]
    ),
}

Relative imports

Relative imports don't work in Jinja. It's possible to customise this behaviour via subclassing the Jinja environment, but generally it's easier to use absolute paths relative to a root directory, e.g. packages.

Dictionary iteration

Note: This pattern is not actually present in our codebase, but could still cause issues.

In Nunjucks, objects are considered iterable. In Jinja, to iterate over an object you need to explicitly add .items() or use the items filter.

The items filter doesn't exist in Nunjucks, but you can use the dictsort filter as a workaround that works identically in both template engines.

Bad:

{% for name, value in foo %}
  {{name}}={{value}}
{% endfor %}

OK:

{% for name, value in foo | dictsort %}
  {{name}}={{value}}
{% endfor %}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment