#2244·jinja

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:

patch
- {{ 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:

  1. The last assertion fails because is escaped incorrectly passes for params.description
  2. Editing the test to guard for params.description is defined makes it work
patch
    {%- 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

python
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

python
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