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
- postgresqlBoth 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 }}"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 }}"See also: Ansible Tags: Run Specific Tasks and Roles Selectively (Guide)
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.* variablesExtended 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 |
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 thanlookup('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.
See also: Ansible Variable Precedence: Complete Order of Priority (2026)
Conclusion
For all new Ansible playbooks, use loop:
loop— Modern, explicit, supportsloop_controlwith_items— Legacy, auto-flattens, still works- Migration: Usually just rename
with_itemstoloop; add| flattenif you relied on auto-flattening with_dict→loop+dict2items;with_nested→loop+product
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 Lists
See also
Category: installation