Tests vs Filters
Jinja2 has two related concepts: filters (transform a value, used after |)
and tests (return true/false, used after is). Confusing them is a common
beginner mistake.
JINJA2 — tests vs filters
# Filter — transforms the value
{{{{ " hello " | trim }}}}
# → "hello"
# Test — returns boolean
{{% if name is defined %}}
hello {{{{ name }}}}
{{% endif %}}
# More tests
{{{{ value is none }}}}
{{{{ list is iterable }}}}
{{{{ x is divisibleby 2 }}}}
{{{{ string is match("^db") }}}} {{# regex match #}}
{{{{ string is search("error") }}}} {{# regex search #}}
Manipulating Lists of Dicts
Most variables in real-world Ansible are lists of dictionaries. The map,
selectattr, and rejectattr filters are how you reshape them:
JINJA2 — list-of-dicts patterns
{{# Given:
users:
- {{ name: alice, admin: true, shell: /bin/bash }}
- {{ name: bob, admin: false, shell: /bin/zsh }}
- {{ name: carol, admin: true, shell: /bin/bash }}
#}}
# Pluck just the names
{{{{ users | map(attribute='name') | list }}}}
# → ['alice', 'bob', 'carol']
# Filter to only admins
{{{{ users | selectattr('admin') | list }}}}
# → [{{ name: alice, admin: true, ... }}, {{ name: carol, admin: true, ... }}]
# Filter to non-admins
{{{{ users | rejectattr('admin') | list }}}}
# Match attribute against a value
{{{{ users | selectattr('shell', 'equalto', '/bin/zsh') | list }}}}
# Combine: get names of admins only
{{{{ users | selectattr('admin') | map(attribute='name') | list }}}}
# → ['alice', 'carol']
# Sort by an attribute
{{{{ users | sort(attribute='name') }}}}
Conditional Expressions
JINJA2 — Python-style ternary
# value if condition else other_value
{{{{ "primary" if mysql_role == "primary" else "replica" }}}}
# Used in default — a common idiom for "use X if defined, else Y"
{{{{ custom_port | default(3306) }}}}
# Default that depends on another variable
{{{{ buffer_pool | default((ansible_memtotal_mb * 0.7) | int) }}}}
# Strict default — error if undefined
{{{{ required_var | mandatory }}}}
dict2items / items2dict
Many Ansible features (loops, includes) want a list of dicts but you have a dict. These filters convert in both directions:
YAML — dict2items in a loop
---
- hosts: localhost
vars:
ports:
mysql: 3306
postgres: 5432
redis: 6379
tasks:
- name: Open each port
ansible.posix.firewalld:
port: "{{{{ item.value }}}}/tcp"
state: enabled
permanent: true
loop: "{{{{ ports | dict2items }}}}"
loop_control:
label: "{{{{ item.key }}}} → {{{{ item.value }}}}"
# Reverse direction (less common)
# items2dict converts list of {{key: K, value: V}} dicts back to a flat dict
regex_replace, regex_search, regex_findall
JINJA2 — regex examples
# Replace
{{{{ "db1.example.com" | regex_replace("\\.", "_") }}}}
# → "db1_example_com"
# Search returns the match (or None)
{{{{ "version 8.0.31-debian" | regex_search("\\d+\\.\\d+\\.\\d+") }}}}
# → "8.0.31"
# Find all matches
{{{{ "ports 3306, 33060, 33061" | regex_findall("\\d+") }}}}
# → ['3306', '33060', '33061']
# Capture groups
{{{{ "user=alice" | regex_replace("user=(\\w+)", "\\1") }}}}
# → "alice"
⚠ Warning: Regex inside YAML and Jinja2 needs double escaping — every literal backslash is
\\\\. The example above shows what you actually type. regex_search returns None on no match, which Jinja2 silently prints as empty — always combine with a default or test for safety. from_yaml / to_yaml / to_nice_yaml
JINJA2 — converting between data and YAML
# Pretty-print a dict as YAML in your output
{{{{ users | to_nice_yaml(indent=2) }}}}
# Read a string of YAML back into a dict
{{% set parsed = yaml_string | from_yaml %}}
# Same for JSON
{{{{ data | to_nice_json }}}}
{{% set parsed = json_string | from_json %}}
Custom Filters — Plug Your Own In
If the built-in filters aren't enough, you can write your own in Python and drop them
in filter_plugins/ next to your playbook. Ansible auto-loads them.
PYTHON — filter_plugins/my_filters.py
def to_systemd_env(d):
"""Convert a dict into systemd Environment= lines."""
if not isinstance(d, dict):
raise TypeError("expected a dict")
return "\n".join(f'Environment="{k}={v}"' for k, v in d.items())
class FilterModule(object):
def filters(self):
return {
'to_systemd_env': to_systemd_env,
}
JINJA2 — using the custom filter
{{# In a template #}}
[Service]
{{{{ env_vars | to_systemd_env }}}}
# expands to:
# [Service]
# Environment="DB_HOST=db1"
# Environment="DB_PORT=3306"
✅ Tip: Custom filters keep templates clean — instead of stuffing complex string-mangling logic into Jinja2 expressions, name the operation in Python where it can be unit-tested.