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

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

Related Articles

Ansible loop_control: label, index_var, pause & loop_var GuideAnsible Flatten: Nested Lists in PlaybooksAnsible map Filter: Extract Attributes from ListsAnsible dict2items: Convert Dictionaries to Lists

See also

Paramiko Deprecated for network_cli: Migrate to libssh (ansible-pylibssh)

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home