Skip to content

with_X vs Loop


The very first chapter in this section mentioned with_items in passing and promised a fuller treatment later. This is that chapter — the whole with_X family, and exactly how to translate each one into the loop: you’ve been using throughout this section.

Why with_X Existed, And Why loop: Replaced It

Before loop: existed as a single, unified keyword, Ansible had a whole family of with_<something> keywords, each hardcoded to one specific data source: with_items for a flat list, with_dict for a dictionary, with_fileglob for matching files on disk, with_nested for combining multiple lists, and several more beyond these. loop: replaced all of them with one consistent idea: it always takes a single list, and you shape whatever data source you actually have — a dict, a glob pattern, two lists combined — into that shape using filters or lookup plugins, rather than memorizing a different keyword for every source.

with_listloop

The simplest case — often a literal rename:

# old
with_list:
  - apple
  - banana

# new
loop:
  - apple
  - banana

with_dictloop + dict2items

Already covered in full back in Chapter 4 — worth seeing side by side with its predecessor:

# old
with_dict: "{{ settings }}"

# new
loop: "{{ settings | dict2items }}"

item.key/item.value work identically either way.

with_nested And with_togetherproduct And zip

with_nested produced every possible combination of items across multiple lists — the Cartesian product. Its loop: equivalent is a new filter worth introducing here, product:

# old
with_nested:
  - [1, 2]
  - ['a', 'b']
# produces: [1,'a'], [1,'b'], [2,'a'], [2,'b']

# new
loop: "{{ [1, 2] | product(['a', 'b']) | list }}"

with_together, by contrast, paired corresponding elements from multiple lists — exactly what zip already does:

# old
with_together:
  - [1, 2, 3]
  - ['a', 'b', 'c']

# new
loop: "{{ [1, 2, 3] | zip(['a', 'b', 'c']) | list }}"

with_fileglobloop + The fileglob Lookup

# old
with_fileglob:
  - "/etc/configs/*.conf"

# new
loop: "{{ lookup('fileglob', '/etc/configs/*.conf', wantlist=true) | list }}"

Lookup plugins — the lookup(...) function itself — get their own proper treatment in a later section of this course; for now, just recognize this shape as with_fileglob’s modern replacement. One detail worth flagging immediately: wantlist=true. Without it, lookup() can return a single string (multiple matches joined together) rather than a genuine list, which breaks loop:’s expectations. wantlist=true forces a real list back, every time.

A Gotcha: Flattening Behavior Differs

This is the sharpest real difference between old and new. with_items had special automatic behavior: a nested list inside it got flattened one level automatically. loop: does not do this at all.

# old: with_items auto-flattened one level
with_items:
  - [1, 2]
  - [3, 4]
# iterated as: 1, 2, 3, 4 — four iterations
# new: loop does NOT auto-flatten
loop: "{{ [[1, 2], [3, 4]] }}"
# iterated as: [1, 2], then [3, 4] — only TWO iterations, each a whole sub-list

A direct rename here silently changes behavior — what used to be four flat iterations becomes two iterations, each one now handed an entire sub-list as item instead of a single value. The fix is flatten, from the lists chapter:

loop: "{{ [[1, 2], [3, 4]] | flatten }}"
# iterated as: 1, 2, 3, 4 — matches the old with_items behavior exactly

Any migration of a with_items loop that ever received nested lists needs this explicit | flatten added — without it, the migration isn’t actually equivalent, even though it looks like a harmless rename.

Should You Ever Use with_X?

It’s not broken, and you’ll keep seeing it in older playbooks and roles you didn’t write yourself — worth being able to read comfortably. For anything you’re writing now, though, loop: is the consistent, current, recommended approach across the board.

It’s not that you should not ever use with_X. Ansible recommends for some use cases like:

Any with_* statement that requires using lookup within a loop should not be converted to use the loop keyword. For example, instead of doing:

loop: "{{ lookup('fileglob', '*.txt', wantlist=True) }}"

it is cleaner to keep

with_fileglob: '*.txt'

Yeah, it’s a recommendation but what matters more is your own consistency. For morden usage, if you are using loop everywhere then looking for with_X pattern may require more efforts and understandings.

Best Practices

  • Treat a with_items migration as more than a rename — check whether the original data ever contained nested lists, and add | flatten explicitly if it did.
  • Always add wantlist=true when converting a with_X lookup-based loop to loop: plus lookup(...) directly.
  • Don’t bother converting old, working with_X loops in playbooks you’re not otherwise actively changing — but default to loop: for anything new, without exception.
Last updated on