Ansible 2.19.0 breaks `loop`-templates because of jinja native types.
Summary
After upgrading to Ansible 2.19.0 some of our loop: jinja templates don't work any more and report an Provide a list of items/templates, or a template resolving to a list. error.
Issue Type
Bug Report
Component Name
ansible, lib/ansible/executor/task_executor, TemplateEngine
Ansible Version
$ ansible --version
ansible-playbook [core 2.19.0]
config file = /.../ansible.cfg
configured module search path = ['/.../.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3.13/site-packages/ansible
ansible collection location = /.../.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible-playbook
python version = 3.13.5 (main, Jun 21 2025, 09:35:00) [GCC 15.1.1 20250425] (/usr/bin/python)
jinja version = 3.1.6
pyyaml version = 6.0.2 (with libyaml v0.2.5)
Configuration
# if using a version older than ansible-core 2.12 you should omit the '-t all'
$ ansible-config dump --only-changed -t all
CONFIG_FILE() = /.../ansible.cfg
DEFAULT_LOG_PATH(/.../ansible.cfg) = /.../ansible.log
EDITOR(env: EDITOR) = vim
PAGER(env: PAGER) = less
GALAXY_SERVERS:
OS / Environment
$ uname -a
Linux hostname 6.15.7-arch1-1 #1 SMP PREEMPT_DYNAMIC Thu, 17 Jul 2025 21:05:29 +0000 x86_64 GNU/Linux
Steps to Reproduce
This playbook works as expected in previous ansible versions, but fails in 2.19.0 claiming the loop template does not resolve to a list.
---
- name: Test loop jinja template
hosts: localhost
vars:
somevar:
somefile1:
typea:
- mail1
- mail2
typeb:
- mail3
tasks:
- debug:
msg: "File: {{ item.file }} Type: {{ item.type }} mails: {{ item.mails }}"
loop: |
[
{% for file, types in somevar.items() %}
{% for type, mails in types.items() %}
{"file": "{{ file }}", "type": "{{ type }}", "mails": {{ mails }} },
{% endfor %}
{% endfor %}
]
But the provided template does resolve to a list, even when using a jinja2 NativeEnvironemnt:
import jinja2.nativetypes
T = """[
{% for file, types in somevar.items() %}
{% for type, mails in types.items() %}
{"file": "{{ file }}", "type": "{{ type }}", "mails": {{ mails }} },
{% endfor %}
{% endfor %}
]"""
env = jinja2.nativetypes.NativeEnvironment()
data = {
"somefile1": {
"typea": [
"mail1",
"mail2",
],
"typeb": [
"mail3",
]
},
}
result = env.from_string(T).render(somevar=data)
print(type(result)) #Prints:` <class 'list'>
print(result)
I suspect that at some place in the ansible pipeline some whitespace sneaks into the template, which makes it resolve to a str and not a list.
Even flatting the whole loop template to a single line and wrapping it in ' still reproduces the error, though.
Expected Results
The playbook should complete successfully looping over the values.
Actual Results
ansible-playbook [core 2.19.0]
config file = /.../ansible.cfg
configured module search path = ['/.../.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3.13/site-packages/ansible
ansible collection location = /.../.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible-playbook
python version = 3.13.5 (main, Jun 21 2025, 09:35:00) [GCC 15.1.1 20250425] (/usr/bin/python)
jinja version = 3.1.6
pyyaml version = 6.0.2 (with libyaml v0.2.5)
Using /.../ansible.cfg as config file
host_list declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
script declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
auto declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
yaml declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
ini declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
toml declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
Skipping callback 'minimal', as we already have a stdout callback.
Skipping callback 'oneline', as we already have a stdout callback.
PLAYBOOK: test_loop_template.yml ***************************************************************************************************************************
1 plays in test_loop_template.yml
PLAY [Test loop jinja template] ****************************************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************************************************
task path: /.../test_loop_template.yml:2
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: user
<127.0.0.1> EXEC /bin/sh -c 'echo ~user && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /.../.ansible/tmp `"&& mkdir "` echo /.../.ansible/tmp/ansible-tmp-1753963312.4276824-225544-267776214024951 `" && echo ansible-tmp-1753963312.4276824-225544-267776214024951="` echo /.../.ansible/tmp/ansible-tmp-1753963312.4276824-225544-267776214024951 `" ) && sleep 0'
Using module file /usr/lib/python3.13/site-packages/ansible/modules/setup.py
<127.0.0.1> PUT /.../.ansible/tmp/ansible-local-225541klf5vx7c/tmpodvhhgt1 TO /.../.ansible/tmp/ansible-tmp-1753963312.4276824-225544-267776214024951/AnsiballZ_setup.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+rwx /.../.ansible/tmp/ansible-tmp-1753963312.4276824-225544-267776214024951/ /.../.ansible/tmp/ansible-tmp-1753963312.4276824-225544-267776214024951/AnsiballZ_setup.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python /.../.ansible/tmp/ansible-tmp-1753963312.4276824-225544-267776214024951/AnsiballZ_setup.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c 'rm -f -r /.../.ansible/tmp/ansible-tmp-1753963312.4276824-225544-267776214024951/ > /dev/null 2>&1 && sleep 0'
ok: [localhost]
TASK [debug] ***********************************************************************************************************************************************
task path: /.../test_loop_template.yml:18
[ERROR]: The `loop` value must resolve to a 'list', not 'str'.
Origin: /.../test_loop_template.yml:20:11
18 - debug:
19 msg: "File: {{ item.file }} Type: {{ item.type }} mails: {{ item.mails }}"
20 loop: |
^ column 11
Provide a list of items/templates, or a template resolving to a list.
fatal: [localhost]: FAILED! => {
"msg": "The `loop` value must resolve to a 'list', not 'str'."
}
PLAY RECAP *************************************************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
Code of Conduct
- I agree to follow the Ansible Code of Conduct
Source: ansible/ansible