Setting Defaults and Handling Missing Data
This closing chapter is about a question every one of the previous six has quietly assumed an answer to: what happens when the data you expected simply isn’t there? default and mandatory handle that at the level of a single value; combine, already covered in the dictionaries chapter, handles it at the level of a whole configuration; and omit handles a specific case unique to Ansible itself — a module parameter that shouldn’t be passed at all.
default — The Simplest Fallback
Already introduced back in the Jinja2 basics chapter — worth revisiting properly now that it’s the closing theme of this section:
msg: "{{ nickname | default('friend') }}"If nickname is undefined, this prints friend instead of failing. Simple, and correct for exactly one specific situation — covered next.
A Gotcha: default Doesn’t Catch Everything
default, used plainly like above, only activates when a variable is genuinely undefined — not when it’s defined but empty:
vars:
username: ""
tasks:
- name: This does NOT use the fallback
ansible.builtin.debug:
msg: "{{ username | default('anonymous') }}"""username exists — it’s just an empty string — so default considers it already defined and leaves it alone entirely. If you want an empty string (or 0, or false, or an empty list) to also trigger the fallback, pass true as a second argument:
msg: "{{ username | default('anonymous', true) }}"anonymousThis second form treats any falsy value as needing the default, not just a strictly undefined one. Knowing which of these two you actually want — “undefined only” or “undefined or empty” — matters a great deal, and it’s an easy detail to overlook the first several times you reach for default.
mandatory — Failing Loudly On Purpose
Sometimes the right response to a missing variable isn’t a fallback at all — it’s an immediate, clear failure, rather than letting a script continue with something silently wrong and fail confusingly much later:
vars:
api_key: "{{ undefined_variable | mandatory }}"
tasks:
- name: Use it
ansible.builtin.debug:
msg: "{{ api_key }}"This fails immediately, at the exact point the missing variable is used, with a clear message stating the variable is required. You can supply your own message too:
msg: "{{ undefined_variable | mandatory('You must supply api_key via -e or a vars file') }}"This is worth reaching for anywhere a variable is genuinely required for a playbook to make sense at all — failing fast, with a clear reason, is far easier to debug than a confusing error three tasks later caused by an empty value that should never have been allowed through.
Merging Partial Data With Defaults Using combine
This is combine, from the dictionaries chapter, applied to exactly the use case it’s best suited for: a full set of sensible defaults, merged with whatever a user actually chose to specify:
vars:
defaults:
timeout: 30
retries: 3
verbose: false
user_config:
timeout: 60
tasks:
- name: Merge user config over defaults
ansible.builtin.debug:
msg: "{{ defaults | combine(user_config, recursive=True) }}"{"timeout": 60, "retries": 3, "verbose": false}The user only specified timeout — everything else fell through from defaults untouched, and recursive=True (from that earlier chapter’s gotcha) makes sure this holds even if defaults or user_config contain nested dictionaries of their own. This pattern — a defaults dictionary, combined with a partial override — is the standard, structural way to “set defaults” for anything more complex than a single value.
omit — Leaving Out An Optional Module Parameter
omit is unique to Ansible, and solves a problem specific to module parameters: sometimes you want a parameter left out entirely — not passed as an empty string, not passed as null — when a variable isn’t set:
- name: Conditionally include a parameter
ansible.builtin.command:
cmd: "echo hello"
chdir: "{{ working_directory | default(omit) }}"If working_directory isn’t defined, chdir is treated as though it was never written into the task at all. This matters because some module parameters don’t handle an empty string or null gracefully — they specifically expect either a real value or genuine absence, and default(omit) is exactly the tool for giving them that.
Best Practices
- Use
default(value, true), not justdefault(value), when empty or falsy values should also trigger the fallback — don’t assume the plain form catches everything undefined-adjacent. - Use
mandatoryfor genuinely required variables, so a missing one fails immediately and clearly, rather than causing a confusing failure somewhere else entirely. - Use
defaults | combine(user_supplied, recursive=True)as your standard pattern for merging a full configuration with partial overrides — this is the samecombinefrom the dictionaries chapter, put to its most natural use. - Use
default(omit)specifically for optional module parameters, not for ordinary variables in your own logic — it’s a narrow, Ansible-specific tool for exactly that one situation.
That closes out this section on manipulating data — strings, numbers, lists, dictionaries, filtering and reshaping collections, converting between formats, and now, defaults. The next section picks up exactly where this chapter’s mandatory and default examples left a thread dangling: conditionals with when:, and the Jinja2 tests — is defined, is string, and the rest — that were deliberately set aside back in the filtering chapter.