AnsibleXXVIII · Plugins, Lookups and FiltersPlugins, lookups and filters
lookup, query and loops
What you'll learn
- Predict the type a lookup returns and prove it with type_debug
- Choose between lookup, query and wantlist=True for a given use
- Recognise the loop failure a string-returning lookup causes
- Use the errors option deliberately rather than inheriting strict behaviour
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
lookup() and query() call the same plugin with the same arguments
and return different types. That is the whole lesson, and it is worth a
lesson because the difference is invisible in the common case and
catastrophic in the loop case.
The demonstration
- name: lookup versus query
hosts: localhost
gather_facts: false
vars:
pkgs: ['nginx', 'curl', 'jq']
tasks:
- ansible.builtin.debug:
msg: "{{ lookup('ansible.builtin.items', pkgs) }}"
- ansible.builtin.debug:
msg: "{{ lookup('ansible.builtin.items', pkgs) | type_debug }}"
- ansible.builtin.debug:
msg: "{{ query('ansible.builtin.items', pkgs) }}"
- ansible.builtin.debug:
msg: "{{ query('ansible.builtin.items', pkgs) | type_debug }}"$ ansible-playbook -i localhost, lq.ymlTASK [lookup returns a string] *************************************************
ok: [localhost] => {
"msg": "nginx,curl,jq"
}
TASK [type of lookup result] ***************************************************
ok: [localhost] => {
"msg": "str"
}
TASK [query returns a list] ****************************************************
ok: [localhost] => {
"msg": [
"nginx",
"curl",
"jq"
]
}
TASK [type of query result] ****************************************************
ok: [localhost] => {
"msg": "list"
}"nginx,curl,jq" — one string, with commas in it. Not a list that
prints with commas. The type_debug filter is the proof and it is
worth reaching for any time an expression behaves oddly; it answers
“what is this actually” in one filter.
lookup() joins. It takes whatever the plugin returned and
comma-joins it into a string, because the historical use of a lookup
was to produce a scalar for a template.
query() does not. It returns the plugin’s list as a list. It is
an alias for lookup(..., wantlist=True):
$ ansible-playbook -i localhost, wantlist.ymlTASK [wantlist type] ***********************************************************
ok: [localhost] => {
"msg": "list vs str"
}The loop case
This is where it stops being a curiosity:
- ansible.builtin.debug:
msg: "item is {{ item }}"
loop: "{{ lookup('ansible.builtin.items', pkgs) }}"$ ansible-playbook -i localhost, lq.ymlTASK [loop over lookup] ********************************************************
[ERROR]: The `loop` value must resolve to a 'list', not 'str'.
Origin: /home/opsuser/project/lq.yml:22:13
20 ansible.builtin.debug:
21 msg: "item is {{ item }}"
22 loop: "{{ lookup('ansible.builtin.items', pkgs) }}"
^ column 13
Provide a list of items/templates, or a template resolving to a list.
fatal: [localhost]: FAILED! => {"msg": "The `loop` value must resolve to a 'list', not 'str'."}A clear error, pointing at the exact line and column, saying precisely what is wrong. That is a good outcome and it is not one you can rely on having in older environments — the historic behaviour of feeding a string to something expecting a sequence is to iterate it, one character at a time.
Which to use
| Situation | Use |
|---|---|
| Interpolating a value into a template or a string | lookup() |
Feeding loop: | query() |
| Setting a variable that other tasks will iterate | query() |
| A module parameter that takes a list | query() |
| A module parameter that takes a scalar | lookup() |
| Any lookup that can return more than one result | query(), then decide |
The default worth adopting: query() unless you specifically want a
joined string. A single-element list is easy to handle; a string that
should have been a list is a bug that appears months later.
Note that query() always returns a list even for lookups that
conceptually return one value:
$ ansible-playbook -i localhost, scalar.ymlTASK [single item still a string] **********************************************
ok: [localhost] => {
"msg": "list / str"
}So query('env', 'HOME') gives ['/home/opsuser'], not
'/home/opsuser'. For a genuinely scalar read, lookup() is the right
call and | first on a query result is a smell.
errors
Every lookup accepts an errors argument controlling what happens when
it cannot do its job. The default is strict.
$ ansible-playbook -i localhost, err.ymlTASK [errors ignore] ***********************************************************
ok: [localhost] => {
"msg": "got []"
}
TASK [errors warn] *************************************************************
[WARNING]: An error occurred while running the lookup plugin 'ansible.builtin.file': Unable to access the file 'nope.txt': File not found. Use -vvvvv to see paths searched.
ok: [localhost] => {
"msg": "got []"
}
TASK [errors strict (default)] *************************************************
[ERROR]: Task failed: Finalization of task args for 'ansible.builtin.debug' failed: Error while resolving value for 'msg': The lookup plugin 'ansible.builtin.file' failed: Unable to access the file 'nope.txt': File not found. Use -vvvvv to see paths searched.
fatal: [localhost]: FAILED! => {"msg": "Task failed: Finalization of task args for 'ansible.builtin.debug' failed: Error while resolving value for 'msg': The lookup plugin 'ansible.builtin.file' failed: Unable to access the file 'nope.txt': File not found."}strict(default) — the lookup failure fails the task. Correct for anything the play depends on.warn— logs a warning and yields empty. The failure is visible in the run output and does not stop it.ignore— yields empty silently.got []with no indication anything went wrong.
Knowledge check
Knowledge check · 4 questions
Q1. What does lookup('ansible.builtin.items', ['nginx', 'curl', 'jq']) return?
Q2. Which use of a string-returning lookup is most dangerous?
Q3. Which statements about the errors argument to a lookup are correct? Select all that apply.
Q4. query() returns a list even for a lookup that conceptually yields a single value, such as an environment variable.
Passing score: 75%. Answers are checked in this browser.