Filters and Tests¶
What You'll Learn¶
- The difference between a filter (
|) and a test (is) - The dozen filters that cover most real playbooks, with examples
- How to filter and reshape lists of dictionaries without writing loops
- Precedence traps that make a filter apply to less than you think
Why This Exists¶
Filters transform a value (| default, | join); tests answer a yes/no question about a value (is defined, is failed) — together they're most of what makes a Jinja2 expression in a real playbook longer than a bare variable name.
Mental Model¶
A filter takes a value in and gives a new value out:
value | filter(args). A test takes a value in and gives true or false out:value is test. Filters chain left to right like a shell pipeline; tests are used inwhen:,failed_when:, and{% if %}.
"{{ packages | unique | sort | join(', ') }}" # filter chain → a string
when: result is failed # test → boolean
Defaults and Required Values¶
# Fallback when the variable is undefined
listen_port: "{{ http_port | default(80) }}"
# Also treat empty/falsy values ("", [], 0) as missing
admin_email: "{{ owner_email | default('ops@example.com', true) }}"
# Omit a module parameter entirely when no value was given
- ansible.builtin.user:
name: deploy
shell: "{{ user_shell | default(omit) }}"
# Fail with a clear message instead of a vague undefined error
db_password: "{{ vault_db_password | mandatory('vault_db_password must be set in vault.yml') }}"
Strings and Lists¶
| Filter | Example | Result |
|---|---|---|
join |
['a','b'] \| join(',') |
a,b |
length |
['a','b'] \| length |
2 |
unique |
[1,1,2] \| unique |
[1, 2] |
sort |
['b','a'] \| sort |
['a', 'b'] |
first / last |
groups['web'] \| first |
web01 |
flatten |
[[1,2],[3]] \| flatten |
[1, 2, 3] |
difference / union / intersect |
[1,2,3] \| difference([2]) |
[1, 3] |
lower / upper / trim |
' Web ' \| trim \| lower |
web |
replace |
'a-b' \| replace('-', '_') |
a_b |
regex_replace |
'v1.2.3' \| regex_replace('^v', '') |
1.2.3 |
regex_search |
'build-4521' \| regex_search('\\d+') |
4521 |
int / float / bool / string |
'3' \| int |
3 |
basename / dirname |
'/opt/app/releases/2.1' \| basename |
2.1 |
ternary |
is_prod \| ternary('warning', 'debug') |
warning |
Looping Over Dictionaries: dict2items and items2dict¶
loop: needs a list. Turn a dictionary into one:
vars:
sysctl_settings:
net.core.somaxconn: 4096
vm.swappiness: 10
tasks:
- name: Apply kernel settings
ansible.posix.sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
state: present
loop: "{{ sysctl_settings | dict2items }}"
And back again:
Selecting and Projecting Lists of Dictionaries¶
Real data is usually a list of dictionaries:
vars:
services:
- { name: checkout, port: 8080, enabled: true, tier: web }
- { name: payments, port: 8081, enabled: false, tier: web }
- { name: reports, port: 9000, enabled: true, tier: batch }
# Keep only enabled services
enabled: "{{ services | selectattr('enabled') | list }}"
# Keep only web-tier services
web: "{{ services | selectattr('tier', 'equalto', 'web') | list }}"
# Drop disabled ones
active: "{{ services | rejectattr('enabled', 'false') | list }}"
# Project one attribute from each: ['checkout', 'reports']
names: "{{ services | selectattr('enabled') | map(attribute='name') | list }}"
# Ports of enabled web services, as a comma-separated string
ports: "{{ services | selectattr('enabled') | selectattr('tier', 'equalto', 'web') | map(attribute='port') | join(',') }}"
select/reject do the same for plain lists using a test: ports | select('greaterthan', 1024) | list.
End with | list
selectattr, map, and select return lazy generators. Add | list when you store the result or pass it to a module, or you'll see <generator object ...>.
Serialization¶
- ansible.builtin.copy:
dest: /etc/checkout/config.json
content: "{{ app_config | to_nice_json(indent=2) }}"
- ansible.builtin.set_fact:
api_payload: "{{ response.content | from_json }}"
to_json/to_yaml produce compact output; to_nice_json/to_nice_yaml produce readable, indented output for config files.
Tests¶
| Test | True when |
|---|---|
is defined / is undefined |
The variable exists / doesn't |
is none |
The value is null |
is failed / is succeeded |
A registered result failed / succeeded |
is changed / is skipped |
A registered result changed / was skipped |
is string / is number / is mapping / is iterable |
Type checks |
is match('^web') / is search('prod') |
Regex match at start / anywhere |
is version('2.0', '>=') |
Version comparison |
is in |
Membership: 'web' is in group_names |
- name: Check whether the app is installed
ansible.builtin.command: /opt/checkout/bin/checkout --version
register: version_check
failed_when: false
changed_when: false
- name: Install when missing or older than 2.0
ansible.builtin.include_tasks: install.yml
when: >
version_check is failed
or not (version_check.stdout | regex_search('[0-9.]+') is version('2.0', '>='))
Precedence Traps¶
A filter binds tighter than arithmetic and comparison operators:
# Applies default only to `extra` — if `base` is undefined, this still fails
total: "{{ base + extra | default(0) }}"
# What you meant
total: "{{ (base | default(0)) + (extra | default(0)) }}"
Likewise, not x is defined reads as not (x is defined), but mixing not, and, and filters without parentheses gets unreadable fast — add parentheses.
Common Mistakes¶
- Writing
a + b | default(0)and expecting the default to cover the whole expression — it only coversb. - Confusing
is defined(variable exists) with a falsy-value check — an empty string or0is still "defined." - Forgetting
| listafterselectattr/map, passing a generator object to a module. - Using
| default('')for a module parameter that should be absent — use| default(omit). - Comparing version strings with
>(string comparison:"10.0" < "9.0") instead of theversiontest.
Interview Questions¶
- What does
| default([], true)do differently from plain| default([])? - How would you filter a list of dictionaries down to only the ones matching a condition, without a full
forloop? - What's the difference between
default('')anddefault(omit)for a module parameter? - Why does
"{{ '10.0' > '9.0' }}"evaluate to false, and how do you compare versions correctly?
Next¶
Continue to Templates for Config Generation.