Variable State Tests
Back in the collections chapter, selectattr('status', 'equalto', 'running') used a Jinja2 test without explaining what one actually is. This chapter delivers on that — starting with the tests you’ll reach for most: checking whether a variable exists at all, and what type it actually holds.
is defined And is undefined
when: some_optional_var is definedwhen: some_optional_var is undefinedStraightforward — does this variable exist in the current context or not.
is none
A variable can be defined and still hold no real value — a YAML key written with nothing after it, or a field that’s legitimately null. is none catches that specific case, distinct from is undefined:
vars:
maybe_value:
tasks:
- name: This is defined, but empty
ansible.builtin.debug:
msg: "It's None"
when: maybe_value is nonemaybe_value is defined would be true here — the variable exists. maybe_value is none is what actually tells you it has no meaningful value.
Combining is defined With when: — A Real Guard Pattern
This is the practical payoff: prevent a task from even attempting to use a variable that might not exist, avoiding an undefined-variable error entirely:
- name: Use optional_config only if it's actually set
ansible.builtin.debug:
msg: "Config value: {{ optional_config }}"
when: optional_config is definedWithout the when: guard, referencing {{ optional_config }} in a run where it was never set would fail the task outright. With it, the task is simply skipped — a clean, expected outcome instead of an error.
is string, is number, is mapping, is sequence
Type tests, useful whenever a variable’s shape isn’t guaranteed — command-line extra vars, or a value that came through from_json from the formats chapter, are both common sources of this uncertainty:
when: some_var is string
when: some_var is number
when: some_var is mapping
when: some_var is sequencemapping checks for a dictionary-like value; sequence checks for an ordered collection.
Warning
is sequence is true for both lists and strings — a plain string is technically a sequence of characters, in exactly the same sense a list is a sequence of items. If you specifically need “is this a list, and not just a string,” is sequence alone doesn’t give you that distinction cleanly. Worth remembering before assuming is sequence means “is a list.”
A Real-World Use: Validating Parsed Data
Recall from_json from the formats chapter — a string parsed into structured data, but with no guarantee the result is actually shaped the way you expect:
vars:
parsed_config: "{{ some_json_string | from_json }}"
tasks:
- name: Only proceed if the config parsed into a proper mapping
ansible.builtin.debug:
msg: "Config looks good: {{ parsed_config }}"
when: parsed_config is mappingIf some_json_string happened to be valid JSON but not actually an object — a bare number, or a JSON array — this guard catches that before anything downstream assumes it can treat parsed_config like a dictionary.
is defined vs. | default: Different Tools For Different Moments
Worth being explicit about, since both deal with “a variable might not be there”: is defined (used in when:) decides whether to run something at all. | default(...), from the previous section, provides a fallback value to use instead, without skipping anything. Reach for is defined when the right response to a missing variable is “skip this step entirely.” Reach for | default when the right response is “just use this other value and keep going.” They solve related problems, but they’re not interchangeable.
Best Practices
- Guard any task that references an optional variable with
when: variable is defined, rather than letting an undefined-variable error surface at the point of use. - Don’t assume
is sequencemeans “is a list” — a string passes this test too. - Choose between
is definedand| defaultdeliberately based on whether the right response to missing data is “skip” or “substitute.”