Ansible with_items vs loop: Migration Guide & Key Differences (2026)
By Luca Berton · Published 2024-01-01 · Category: installation
Complete guide to Ansible with_items vs loop. Understand the differences, migrate from with_* to loop, use loop_control, and handle complex iteration patterns.
Ansible has two ways to iterate over data: the modern loop keyword (introduced in Ansible 2.5) and the legacy with_ keywords (with_items, with_dict, with_fileglob, etc.). This guide explains the differences, when to use each, and how to migrate.
Quick Comparison
# Legacy (with_items)
- name: Install packages
ansible.builtin.package:
name: "{{ item }}"
state: present
with_items:
- nginx
- redis
- postgresql
# Modern (loop)
- name: Install packages
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- nginx
- redis
- postgresql
Both produce identical results. The difference is in how they handle complex data.
See also: Ansible Playbook Structure: Anatomy, Best Practices & Examples (2026)
Key Differences
| Feature | with_items | loop |
|---------|-------------|--------|
| Introduced | Ansible 1.x | Ansible 2.5 |
| Auto-flattening | ✅ Yes (1 level) | ❌ No |
| Syntax | with_items: list | loop: list |
| Complex iteration | with_dict, with_nested, etc. | loop + filters |
| Deprecation | Soft-deprecated (still works) | Recommended |
| Performance | Same | Same |
with_items Auto-Flattening
The biggest behavioral difference — with_items automatically flattens one level of nested lists:
vars:
packages:
- nginx
- ['redis', 'postgresql'] # nested list
# with_items FLATTENS → iterates: nginx, redis, postgresql (3 items)
- name: with_items flattens
ansible.builtin.debug:
msg: "{{ item }}"
with_items: "{{ packages }}"
# loop does NOT flatten → iterates: nginx, ['redis', 'postgresql'] (2 items)
- name: loop does not flatten
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ packages }}"
# To get the same behavior with loop, add | flatten
- name: loop with flatten
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ packages | flatten }}"
See also: Ansible Tags: Run Specific Tasks and Roles Selectively (Guide)
Migration: with_ to loop
with_items → loop
# Before
- name: Create users
ansible.builtin.user:
name: "{{ item }}"
with_items: "{{ user_list }}"
# After
- name: Create users
ansible.builtin.user:
name: "{{ item }}"
loop: "{{ user_list }}"
with_list → loop (identical)
# Before
- debug: msg="{{ item }}"
with_list: [1, 2, 3]
# After (same behavior — no flattening)
- debug: msg="{{ item }}"
loop: [1, 2, 3]
with_dict → loop + dict2items
vars:
users:
alice: admin
bob: developer
# Before
- name: Show users
ansible.builtin.debug:
msg: "{{ item.key }} is {{ item.value }}"
with_dict: "{{ users }}"
# After
- name: Show users
ansible.builtin.debug:
msg: "{{ item.key }} is {{ item.value }}"
loop: "{{ users | dict2items }}"
with_nested → loop + product
# Before
- name: Create user directories
ansible.builtin.file:
path: "/home/{{ item[0] }}/{{ item[1] }}"
state: directory
with_nested:
- ['alice', 'bob']
- ['documents', 'downloads']
# After
- name: Create user directories
ansible.builtin.file:
path: "/home/{{ item[0] }}/{{ item[1] }}"
state: directory
loop: "{{ ['alice', 'bob'] | product(['documents', 'downloads']) | list }}"
with_subelements → loop + subelements
vars:
users:
- name: alice
ssh_keys:
- key1.pub
- key2.pub
- name: bob
ssh_keys:
- key3.pub
# Before
- name: Deploy SSH keys
ansible.builtin.authorized_key:
user: "{{ item.0.name }}"
key: "{{ lookup('file', item.1) }}"
with_subelements:
- "{{ users }}"
- ssh_keys
# After
- name: Deploy SSH keys
ansible.builtin.authorized_key:
user: "{{ item.0.name }}"
key: "{{ lookup('file', item.1) }}"
loop: "{{ users | subelements('ssh_keys') }}"
with_sequence → loop + range
# Before
- name: Create numbered files
ansible.builtin.file:
path: "/tmp/file_{{ item }}"
state: touch
with_sequence: start=1 end=5
# After
- name: Create numbered files
ansible.builtin.file:
path: "/tmp/file_{{ item }}"
state: touch
loop: "{{ range(1, 6) | list }}"
with_fileglob → loop + fileglob lookup
# Before
- name: Copy config files
ansible.builtin.copy:
src: "{{ item }}"
dest: /etc/app/
with_fileglob: "files/*.conf"
# After
- name: Copy config files
ansible.builtin.copy:
src: "{{ item }}"
dest: /etc/app/
loop: "{{ lookup('fileglob', 'files/*.conf', wantlist=True) }}"
with_together → loop + zip
# Before
- name: Pair lists
ansible.builtin.debug:
msg: "{{ item.0 }} → {{ item.1 }}"
with_together:
- ['web1', 'web2']
- ['10.0.0.1', '10.0.0.2']
# After
- name: Pair lists
ansible.builtin.debug:
msg: "{{ item.0 }} → {{ item.1 }}"
loop: "{{ ['web1', 'web2'] | zip(['10.0.0.1', '10.0.0.2']) | list }}"
loop_control
The loop keyword works with loop_control for advanced iteration control:
- name: Install packages with progress
ansible.builtin.package:
name: "{{ item.name }}"
state: present
loop: "{{ packages }}"
loop_control:
label: "{{ item.name }}" # Clean output (hide long dicts)
index_var: idx # Loop counter (0-based)
pause: 2 # Wait 2 seconds between iterations
loop_var: pkg_item # Rename 'item' (useful in nested loops)
extended: true # Access ansible_loop.* variables
Extended Loop Variables
With extended: true:
- name: Show loop info
ansible.builtin.debug:
msg: >
Item {{ ansible_loop.index }} of {{ ansible_loop.length }}:
{{ item }}
{% if ansible_loop.first %}(FIRST){% endif %}
{% if ansible_loop.last %}(LAST){% endif %}
loop: [a, b, c]
loop_control:
extended: true
| Variable | Description |
|----------|-------------|
| ansible_loop.index | Current iteration (1-based) |
| ansible_loop.index0 | Current iteration (0-based) |
| ansible_loop.first | true on first iteration |
| ansible_loop.last | true on last iteration |
| ansible_loop.length | Total number of items |
| ansible_loop.revindex | Iterations remaining (1-based) |
| ansible_loop.previtem | Previous item |
| ansible_loop.nextitem | Next item |
See also: Ansible Variable Precedence: Complete Order of Priority (2026)
When to Use with_items vs loop
Use loop (recommended):
• All new playbooks
• Simple list iteration
• When you need loop_control
• When you want explicit behavior (no auto-flattening)
Use with_ (acceptable):
• Legacy playbooks that work correctly
• with_fileglob (simpler than lookup('fileglob', ..., wantlist=True))
• When the filter-based equivalent is significantly more complex
• Maintaining consistency in an existing codebase
Is with_items Deprecated?
with_items and other with_ keywords are not officially deprecated as of Ansible 2.17/ansible-core 2.17. They are considered legacy and the documentation recommends loop, but they still work and will continue to work for the foreseeable future.
The ansible-lint tool flags with_ usage as a warning (rule no-jinja-when) but not as an error.
FAQ
What is the difference between with_items and loop in Ansible?
The main difference is auto-flattening: with_items automatically flattens one level of nested lists, while loop passes data as-is. For simple lists, they behave identically. loop is the modern recommended syntax and supports loop_control for advanced features.
Is with_items deprecated in Ansible?
No, with_items is not officially deprecated and still works in all current Ansible versions. However, it's considered legacy syntax. The Ansible documentation recommends using loop for new playbooks, and ansible-lint may flag with_ usage as a style warning.
How do I convert with_items to loop?
For simple lists, replace with_items directly with loop. If your data has nested lists and you relied on auto-flattening, add | flatten: loop: "{{ my_list | flatten }}". For with_dict, use loop: "{{ my_dict | dict2items }}".
Can I use loop_control with with_items?
No, loop_control only works with the loop keyword. This is one of the main reasons to migrate from with_items to loop — you gain access to label, index_var, pause, extended, and loop_var.
Which is faster, with_items or loop?
Performance is identical. Both iterate in the same way internally. The choice should be based on clarity and features, not performance.
Conclusion
For all new Ansible playbooks, use loop:
• loop — Modern, explicit, supports loop_control
• with_items — Legacy, auto-flattens, still works
• Migration: Usually just rename with_items to loop; add | flatten if you relied on auto-flattening
• with_dict → loop + dict2items; with_nested → loop + product
The filter-based approach with loop is more explicit and composable, making your playbooks easier to understand and maintain.
Related Articles
• Ansible loop_control: label, index_var, pause & loop_var Guide • Ansible Flatten: Nested Lists in Playbooks • Ansible map Filter: Extract Attributes from Lists • Ansible dict2items: Convert Dictionaries to ListsSee also
• Paramiko Deprecated for network_cli: Migrate to libssh (ansible-pylibssh)Category: installation