Skip to content

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 defined
when: some_optional_var is undefined

Straightforward — 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 none

maybe_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 defined

Without 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 sequence

mapping 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 mapping

If 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 sequence means “is a list” — a string passes this test too.
  • Choose between is defined and | default deliberately based on whether the right response to missing data is “skip” or “substitute.”
Last updated on