The use-loop rule in ansible-lint warns when you use the older with_* looping syntax instead of the modern loop keyword. While with_* still works, loop is the recommended syntax for all new playbooks. This guide shows you how to convert every with_* pattern to loop.

Understanding the Error

When ansible-lint finds with_* syntax, it produces:

WARNING  use-loop: Use `loop` instead of `with_items`. (use-loop)
playbook.yml:12 Task/Handler: Install packages

Why the Rule Exists

  • loop is the modern standard — introduced in Ansible 2.5 as a replacement for with_*
  • Consistency — one looping syntax instead of 20+ with_* variants
  • Future-proofing — with_* may eventually be deprecated
  • Readability — loop combined with filters is more explicit about data transformations

Quick Conversion Reference

Old SyntaxNew Syntax
with_items: listloop: "{{ list }}"
with_list: listloop: "{{ list }}"
with_dict: dictloop: "{{ dict | dict2items }}"
with_fileglob: patternloop: "{{ query('fileglob', pattern) }}"
with_filetree: pathloop: "{{ query('filetree', path) }}"
with_together: [a, b]loop: "{{ a | zip(b) | list }}"
with_nested: [a, b]loop: "{{ a | product(b) | list }}"
with_subelements: [list, key]loop: "{{ list | subelements(key) }}"
with_sequence: ...loop: "{{ range(start, end+1) | list }}"
with_random_choice: listloop: "{{ [list | random] }}"
with_first_found: listloop: "{{ query('first_found', list) }}"
with_inventory_hostnames: patternloop: "{{ query('inventory_hostnames', pattern) }}"

Conversion Examples

with_items → loop

The most common conversion:

# OLD — triggers use-loop warning
- name: Install packages
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  with_items:
    - nginx
    - curl
    - git

# NEW — preferred syntax
- name: Install packages
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - curl
    - git

Even better — many modules accept lists directly:

# BEST — no loop needed
- name: Install packages
  ansible.builtin.apt:
    name:
      - nginx
      - curl
      - git
    state: present

with_items (flattened list) → loop + flatten

with_items automatically flattens nested lists. loop does not:

# OLD — with_items flattens automatically
- name: Install packages
  ansible.builtin.apt:
    name: "{{ item }}"
  with_items: "{{ package_lists }}"
  # If package_lists = [['nginx', 'curl'], ['git']], with_items flattens it

# NEW — must explicitly flatten
- name: Install packages
  ansible.builtin.apt:
    name: "{{ item }}"
  loop: "{{ package_lists | flatten }}"

with_dict → loop + dict2items

# OLD
- name: Create users
  ansible.builtin.user:
    name: "{{ item.key }}"
    comment: "{{ item.value.full_name }}"
    groups: "{{ item.value.groups }}"
  with_dict: "{{ users }}"

# NEW
- name: Create users
  ansible.builtin.user:
    name: "{{ item.key }}"
    comment: "{{ item.value.full_name }}"
    groups: "{{ item.value.groups }}"
  loop: "{{ users | dict2items }}"

with_fileglob → loop + query

# OLD
- name: Copy config files
  ansible.builtin.copy:
    src: "{{ item }}"
    dest: /etc/myapp/
  with_fileglob:
    - "files/configs/*.conf"

# NEW
- name: Copy config files
  ansible.builtin.copy:
    src: "{{ item }}"
    dest: /etc/myapp/
  loop: "{{ query('fileglob', 'files/configs/*.conf') }}"

with_together → loop + zip

# OLD
- name: Create mount points
  ansible.builtin.mount:
    path: "{{ item.0 }}"
    src: "{{ item.1 }}"
    fstype: ext4
    state: mounted
  with_together:
    - ['/mnt/data1', '/mnt/data2']
    - ['/dev/sdb1', '/dev/sdc1']

# NEW
- name: Create mount points
  ansible.builtin.mount:
    path: "{{ item.0 }}"
    src: "{{ item.1 }}"
    fstype: ext4
    state: mounted
  loop: "{{ ['/mnt/data1', '/mnt/data2'] | zip(['/dev/sdb1', '/dev/sdc1']) | list }}"

with_nested → loop + product

# OLD
- name: Grant database permissions
  community.mysql.mysql_user:
    name: "{{ item[0] }}"
    host: "{{ item[1] }}"
    priv: "*.*:ALL"
  with_nested:
    - ['user1', 'user2']
    - ['localhost', '192.168.1.%']

# NEW
- name: Grant database permissions
  community.mysql.mysql_user:
    name: "{{ item[0] }}"
    host: "{{ item[1] }}"
    priv: "*.*:ALL"
  loop: "{{ ['user1', 'user2'] | product(['localhost', '192.168.1.%']) | list }}"

with_subelements → loop + subelements

# OLD
- name: Add SSH keys for all users
  ansible.posix.authorized_key:
    user: "{{ item.0.name }}"
    key: "{{ item.1 }}"
  with_subelements:
    - "{{ users }}"
    - ssh_keys

# NEW
- name: Add SSH keys for all users
  ansible.posix.authorized_key:
    user: "{{ item.0.name }}"
    key: "{{ item.1 }}"
  loop: "{{ users | subelements('ssh_keys') }}"

with_sequence → loop + range

# OLD
- name: Create numbered directories
  ansible.builtin.file:
    path: "/data/volume_{{ item }}"
    state: directory
  with_sequence: start=1 end=10 format=%02d

# NEW
- name: Create numbered directories
  ansible.builtin.file:
    path: "/data/volume_{{ '%02d' | format(item) }}"
    state: directory
  loop: "{{ range(1, 11) | list }}"

Controlling loop Output

By default, Ansible prints every loop iteration. Use loop_control to manage output:

- name: Install many packages
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop: "{{ packages }}"
  loop_control:
    label: "{{ item }}"      # What to show in output (reduce noise)
    pause: 1                  # Seconds between iterations
    index_var: idx            # Access loop index
    loop_var: pkg             # Rename 'item' (useful for nested loops)

Disabling the Rule

If you prefer with_* syntax or have a large codebase:

# .ansible-lint
skip_list:
  - use-loop

# Or per-task
- name: Legacy task
  ansible.builtin.debug:
    msg: "{{ item }}"
  with_items: "{{ my_list }}"  # noqa: use-loop

Conclusion

Migrating from with_* to loop is straightforward once you know the filter equivalents. The key patterns are: flatten for with_items on nested lists, dict2items for with_dict, query() for lookup-based with_*, and zip/product/subelements for the combination loops. Update your playbooks incrementally — start with new code using loop, and convert existing with_* as you touch those files.