AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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

Featurewith_itemsloop
IntroducedAnsible 1.xAnsible 2.5
Auto-flattening✅ Yes (1 level)❌ No
Syntaxwith_items: listloop: list
Complex iterationwith_dict, with_nested, etc.loop + filters
DeprecationSoft-deprecated (still works)Recommended
PerformanceSameSame

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.* 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
VariableDescription
ansible_loop.indexCurrent iteration (1-based)
ansible_loop.index0Current iteration (0-based)
ansible_loop.firsttrue on first iteration
ansible_loop.lasttrue on last iteration
ansible_loop.lengthTotal number of items
ansible_loop.revindexIterations remaining (1-based)
ansible_loop.previtemPrevious item
ansible_loop.nextitemNext item

When to Use with_items vs loop

  • 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.

See also: Ansible Variable Precedence: Complete Order of Priority (2026)

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_dictloop + dict2items; with_nestedloop + product
The filter-based approach with loop is more explicit and composable, making your playbooks easier to understand and maintain.

See also

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home