ChainableUndefined causes `is escaped` test to pass for `undefined` child elements
Author: colinrotherhamCreated Aug 27, 2026Updated Aug 27, 2026
Sorry if this has been raised already
When using ChainableUndefined the is escaped test incorrectly passes for undefined child elements
For example, given parent = {} we see parent.child is escaped return true
We've started adding is defined to work around the issue:
- {{ parent.child | safe if parent.child is escaped }}
+ {{ parent.child | safe if parent.child is defined and parent.child is escaped }}I'm one of the maintainers of NHS.UK frontend and we noticed the issue in our Jinja port
We're currently reviewing the workaround above in https://github.com/NHSDigital/nhsuk-frontend-jinja/pull/115
I've extracted steps to reproduce the issue below
Steps to reproduce
Run the pytest example below to see that:
- The last assertion fails because
is escapedincorrectly passes forparams.description - Editing the test to guard for
params.description is definedmakes it work
{%- set description = params.description if params.description is mapping else {
"text": params.description if params.description is string else undefined,
- "html": params.description if params.description is escaped else params.descriptionHtml
+ "html": params.description if params.description is defined and params.description is escaped else params.descriptionHtml
} -%}tests/test_compatibility.py
import inspect
def test_compatibility_escaped(environment):
template_string = inspect.cleandoc("""
{% macro card(params = {}) %}
{#- Support description as string (with deprecated options) #}
{%- set description = params.description if params.description is mapping else {
"text": params.description if params.description is string else undefined,
"html": params.description if params.description is defined and params.description is escaped else params.descriptionHtml
} -%}
{{- description.html | safe if description.html else description.text }}
{% endmacro %}
""")
text = inspect.cleandoc("""
{{ card({
"description": "Example description"
}) }}
""")
text_safe = inspect.cleandoc("""
{{ card({
"description": "<p>Example description</p>" | safe
}) }}
""")
text_nested = inspect.cleandoc("""
{{ card({
"description": {
"text": "Example description"
}
}) }}
""")
html_nested = inspect.cleandoc("""
{{ card({
"description": {
"html": "<p>Example description</p>"
}
}) }}
""")
html_deprecated = inspect.cleandoc("""
{{ card({
"descriptionHtml": "<p>Example description</p>"
}) }}
""")
assert (
environment.from_string(f"{template_string}\n{text}").render()
== "Example description\n"
)
assert (
environment.from_string(f"{template_string}\n{text_safe}").render()
== "<p>Example description</p>\n"
)
assert (
environment.from_string(f"{template_string}\n{text_nested}").render()
== "Example description\n"
)
assert (
environment.from_string(f"{template_string}\n{html_nested}").render()
== "<p>Example description</p>\n"
)
assert (
environment.from_string(f"{template_string}\n{html_deprecated}").render()
== "<p>Example description</p>\n"
)tests/conftest.py
from pathlib import Path
import pytest
from jinja2 import ChainableUndefined, Environment, FileSystemLoader
ROOT = Path(__file__).parent.parent
@pytest.fixture
def environment():
return Environment(
undefined=ChainableUndefined,
loader=FileSystemLoader(ROOT / "nhsuk_frontend_jinja" / "templates"),
trim_blocks=True,
lstrip_blocks=True,
autoescape=True,
)Environment:
- Python version: 3.13.2
- Jinja version: 3.1.6
Source: pallets/jinja