To check whether a variable exists in a Jinja2 template, use the is defined test: {% if my_var is defined %}. Its opposite, is undefined, is also available, and the default() filter lets you substitute a fallback value inline without writing an if block at all. Which one you reach for depends on whether you want to branch, provide a fallback, or fail loudly.
{% if my_var is defined %}...{% endif %} to test for existence, {{ my_var | default('fallback') }} to substitute a value when it is missing, and {% if my_var is defined and my_var is not none %} when you also need to reject explicit null values. In Ansible, the same tests work unchanged.This guide covers the four tools Jinja2 gives you for the job (is defined, is undefined, is none and default()), explains how the template environment’s undefined setting changes what “missing” even means, and walks through the traps people hit most often: nested attribute checks, empty strings that look like missing values, and the difference between “not defined” and “falsy”. Ansible users get a dedicated section because Ansible tweaks a few defaults.
The four tests and filters you need
Jinja2 ships with a set of tests (used with is) and filters (used with |). For existence checks, these are the ones that matter.
| Syntax | What it checks | Result when variable is missing | Result when variable is None |
|---|---|---|---|
x is defined | Variable was passed to the template | False | True |
x is undefined | Variable was not passed | True | False |
x is none | Value is Python None | False | True |
x | default('v') | Substitutes 'v' if undefined | ‘v’ | None (unchanged) |
x | default('v', true) | Substitutes if undefined or falsy | ‘v’ | ‘v’ |
The last two rows are the ones that surprise people. default() only kicks in for undefined variables unless you pass true as the second argument, which turns it into a general “replace anything falsy” filter. The official Jinja2 template documentation lists every test and filter with its exact behavior.
Using is defined and is undefined
The basic pattern branches on existence. Here is a template that renders a greeting only when a user variable was supplied:
{% if user is defined %}
Hello, {{ user.name }}!
{% else %}
Hello, guest!
{% endif %}Rendering it from Python looks like this:
from jinja2 import Environment
env = Environment()
tpl = env.from_string("{% if user is defined %}Hi {{ user }}{% else %}Hi guest{% endif %}")
print(tpl.render(user="Dana")) # Hi Dana
print(tpl.render()) # Hi guestis undefined is the mirror image and reads better when the “missing” branch is the interesting one, for example when you want to emit a warning comment in generated config:
{% if listen_port is undefined %}
# WARNING: listen_port not set, falling back to 8080
listen {{ 8080 }};
{% else %}
listen {{ listen_port }};
{% endif %}Both tests can be negated with not: {% if x is not defined %} is valid and identical to {% if x is undefined %}.
The default() filter for inline fallbacks
When you only need a substitute value, an if block is noise. The default() filter (alias d()) does it in one expression:
server_name {{ server_name | default('localhost') }};
worker_processes {{ workers | d(4) }};
log_level {{ log_level | default('info') | upper }};Remember the rule from the table: by default this filter fires only when the variable is undefined. If the caller passes server_name="" or server_name=None, you get an empty string or the literal text None in your output. To treat those as missing too, pass the boolean second argument:
server_name {{ server_name | default('localhost', true) }};default() before other filters. {{ x | default('') | trim | lower }} is safe for a missing x; {{ x | trim | default('') }} is not, because trim runs first on an undefined value.Undefined vs StrictUndefined: what “missing” means
Everything above assumes the default Undefined class. When Jinja2 cannot find a variable, it does not raise immediately. It returns an Undefined object that renders as an empty string, evaluates as falsy, and only raises an UndefinedError if you try to do something with it like call a method or access an attribute. That is forgiving, and it is why {{ missing_var }} silently prints nothing.
You can change this behavior when you build the environment. The important options are:
| Class | Printing a missing variable | is defined still works? | Typical use |
|---|---|---|---|
Undefined (default) | Empty string | Yes | Web templates, tolerant output |
ChainableUndefined | Empty string, and a.b.c on a missing a also renders empty | Yes | Deep optional data structures |
DebugUndefined | Prints {{ name }} as a placeholder | Yes | Debugging which variables are unset |
StrictUndefined | Raises UndefinedError | Yes | Config generation, CI, anything where silence is dangerous |
Set it like this:
from jinja2 import Environment, StrictUndefined
env = Environment(undefined=StrictUndefined)
tpl = env.from_string("{{ missing }}")
tpl.render() # jinja2.exceptions.UndefinedError: 'missing' is undefinedThe key point: is defined, is undefined and default() all keep working under StrictUndefined. The strict class only raises when you try to print, iterate, or otherwise use the undefined value. So a template written with proper existence checks is portable across both modes, which is a strong argument for writing them in the first place.
StrictUndefined, even {% if missing %} raises, because evaluating truthiness counts as using the value. Always write {% if missing is defined and missing %} in strict environments.Checking nested attributes and dictionary keys
A very common failure: {% if config.database.host is defined %} throws an error when config itself is missing, because Jinja2 has to resolve config.database before it can test host. There are three ways to handle it.
The first is to test each level. It is verbose but works everywhere, including Ansible:
{% if config is defined and config.database is defined and config.database.host is defined %}
host = {{ config.database.host }}
{% endif %}The second is to switch the environment to ChainableUndefined, which lets attribute access on a missing value return another undefined instead of raising. Then a single test at the end of the chain is enough:
from jinja2 import Environment, ChainableUndefined
env = Environment(undefined=ChainableUndefined)
# now this is safe even if config is missing entirely:
# {% if config.database.host is defined %}The third is to use dictionary methods when the data is a dict. {{ config.get('database', {}).get('host', 'localhost') }} reads awkwardly, but it never raises. For mapping keys, you can also use the in operator: {% if 'host' in config.database %}. This checks key membership, which is different from is defined but often what you actually mean. If you need to test several keys at once, the pattern in our guide to unpacking more than one variable in Jinja2 pairs well with these checks.
Ansible specifics
Ansible uses Jinja2 for templates and for when: conditions, and everything above applies, with three things worth knowing.
First, Ansible variables that are declared but set to null are defined, not undefined. A play with vars: { db_pass: null } makes db_pass is defined true. That is why the idiomatic Ansible check for “has a real value” is the combined form:
- name: Configure database password
ansible.builtin.template:
src: db.conf.j2
dest: /etc/app/db.conf
when: db_pass is defined and db_pass is not none and db_pass | length > 0Second, Ansible’s default() filter behaves like Jinja2’s, and Ansible adds a special sentinel: {{ var | default(omit) }} tells a module to leave the parameter out entirely rather than pass an empty value. This is the standard way to make module arguments optional.
- name: Create user
ansible.builtin.user:
name: "{{ item.name }}"
shell: "{{ item.shell | default(omit) }}"
groups: "{{ item.groups | default(omit) }}"
loop: "{{ users }}"Third, Ansible defaults to erroring on undefined variables in templates (the DEFAULT_UNDEFINED_VAR_BEHAVIOR setting, which is True by default). That is effectively strict mode, so the advice about always pairing truthiness with is defined matters even more. The Ansible tests documentation also lists Ansible only tests such as is truthy and is falsy, which normalize strings like “yes” and “0”.
Truthiness pitfalls
The most common bug in this area is using {% if x %} as an existence check. It is not one. That expression is false when x is undefined, but it is also false when x is 0, 0.0, an empty string, an empty list, an empty dict, or False. If retries: 0 is a legitimate setting, {% if retries %} throws it away.
{% if port %} silently drops a port of 0 and a debug flag of False. Use {% if port is defined %} for existence and {% if port is not none %} for null checks; reserve bare truthiness for genuine on/off semantics.A related trap involves numeric comparisons on values that may be strings. YAML and environment variables often hand you "0", which is truthy. If you then compare it with a number, results get confusing; our article on comparing float numbers in Jinja2 covers the float and int filters you should apply first. And when the value you are testing needs pattern matching rather than a plain existence check, the match and search tests described in how to use regular expressions in Jinja2 should also be wrapped in an is defined guard.
Troubleshooting
UndefinedError: ‘dict object’ has no attribute ‘x’
You tested a nested attribute without testing its parent, or you are in strict mode and used a bare {% if x %}. Test each level (a is defined and a.b is defined), switch to ChainableUndefined, or use a.get('b') on dicts.
default() is not replacing my empty value
The value is defined but empty or None. Pass the second argument: {{ x | default('fallback', true) }}. If you specifically want to replace only None and keep empty strings, write {{ x if x is not none else 'fallback' }}.
is defined returns true but the output is blank
The variable exists and holds an empty string, an empty list, or None. Existence and content are separate checks. Add and x | length > 0 or and x is not none as appropriate.
Ansible skips a task even though the variable is set in group_vars
Check precedence. A higher precedence source (extra vars, set_fact, host_vars) may define the variable as null or empty. Run ansible -m debug -a "var=your_var" host to see the resolved value before the task runs.
Template renders “None” as literal text
You printed a defined variable whose value is None. Jinja2 does not blank it out. Use {{ x | default('', true) }} or {{ x if x is not none else '' }}.
Frequently asked questions
Is there a difference between “is defined” and “is not undefined”?
No. They are logically equivalent, and both are built from the same defined and undefined tests. Pick whichever reads more naturally in context. Most style guides prefer is defined for the positive case and is not defined for the negative, keeping undefined for cases where the missing branch is the main path.
Does “is defined” work on dictionary keys?
Yes, through attribute or subscript syntax: {% if mydict.key is defined %} and {% if mydict['key'] is defined %} both work, as long as mydict itself exists. If it may not, guard the parent first or use 'key' in mydict combined with mydict is defined.
Can I check if a variable is an empty string rather than missing?
Use {% if x is defined and x == '' %} or the length filter: {% if x is defined and x | length == 0 %}. The length version also covers empty lists and dicts, which makes it handy when the type varies between callers.
How do I check multiple variables at once?
Combine tests with and or or: {% if host is defined and port is defined %}. For a list of names, loop with {% for name in ['host','port'] %} and use vars lookup in Ansible, or build the check in Python before rendering. Jinja2 itself has no built in “all defined” shortcut.
Does StrictUndefined break the default() filter?
No. default() is designed to accept an undefined value and return the fallback before anything tries to use it. The same is true for is defined. Strict mode only raises when an undefined value is printed, iterated, compared, or has attributes accessed, which is exactly the behavior you want in configuration templates.
The bottom line
Use is defined when you need to branch on whether a variable was passed, default() when you only need a fallback value, and add is not none or the true flag on default() when null and empty values should count as missing. Never use bare truthiness as an existence test, because zero and empty are valid values in most real configurations.
If you generate anything that matters (server config, infrastructure code, CI files), turn on StrictUndefined in Python or leave Ansible’s default strictness in place. Templates written with proper existence checks work identically in both modes, and the strict mode will catch the typo in a variable name long before it reaches production.

