Ansible Error Guide: 'dict object' has no attribute (Undefined Variable)
Fix Ansible's FAILED! 'dict object' has no attribute error: diagnose undefined variables, typos in var names, missing facts, wrong scope, and unset registered results.
- #ansible
- #troubleshooting
- #errors
- #variables
Overview
This error is Ansible (via Jinja2) telling you a template referenced a key that does not exist on a dictionary. You asked for myvar.something or result.stdout, but myvar/result either is not defined or does not contain that attribute, so Jinja2 raises AnsibleUndefined and the task fails.
You will see it on a task that uses a template, set_fact, when, or any module argument with {{ }}:
fatal: [web-01]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'dict object' has no attribute 'stdout'. 'dict object' has no attribute 'stdout'\n\nThe error appears to be in '/home/deploy/site/roles/app/tasks/main.yml': line 22, column 7, ...
Two variants show up: 'dict object' has no attribute '<key>' (the dict exists but lacks that key) and '<name>' is undefined (the variable was never defined at all). Both mean the same class of bug — a reference that resolves to nothing.
Symptoms
- A task fails with
undefined variable/has no attributeand points at a specific line/column. - The play worked on one host but fails on another (a host-specific fact or registered var is missing).
- The failure is on a
when,template,set_fact, or a module arg using{{ }}. - Adding
-vvvshows the partially-rendered template right before the error.
ansible-playbook -i inventory.ini site.yml --limit web-01
TASK [app : Write app config] **************************************************
fatal: [web-01]: FAILED! => {"msg": "AnsibleUndefined: 'dict object' has no attribute 'release'"}
Common Root Causes
1. Typo in the variable or attribute name
The most common cause: the key you typed does not match the key that exists.
# defines app_version, but the template asks for app_verison
vars:
app_version: "2.4.1"
'dict object' has no attribute 'app_verison'
Dump the var to compare the real name against what you referenced:
ansible web-01 -i inventory.ini -m debug -a "var=app_version"
2. Registered result accessed before it was set
A register only populates on the task that runs. If the task was skipped (a when was false) or you reference a subkey it does not have, the attribute is missing.
- command: cat /etc/release
register: rel
when: ansible_os_family == "RedHat"
- debug:
msg: "{{ rel.stdout }}" # rel.stdout is undefined on Debian hosts
'dict object' has no attribute 'stdout'
A skipped task still defines rel, but as {"skipped": true, "changed": false} — no stdout.
3. Fact not gathered (gather_facts disabled or filtered)
Referencing ansible_facts / ansible_* when fact gathering is off, or asking for a fact that was filtered out by gather_subset.
- hosts: web
gather_facts: false
tasks:
- debug:
msg: "{{ ansible_default_ipv4.address }}"
'ansible_default_ipv4' is undefined
ansible web-01 -i inventory.ini -m setup -a 'filter=ansible_default_ipv4'
4. Variable defined in the wrong scope / host
A var set in host_vars/web-01 or in a different play is not visible where you use it. group_vars and host_vars are per-host.
ansible web-02 -i inventory.ini -m debug -a "var=db_host"
web-02 | SUCCESS => {
"db_host": "VARIABLE IS NOT DEFINED!"
}
db_host exists for web-01 but not web-02 — referencing it on web-02 raises the error.
5. Nested key on a dict that is sometimes empty
A nested lookup like users[item].shell fails when item is not a key in users, or users is {} on some hosts.
- debug:
msg: "{{ users['deploy'].shell }}"
'dict object' has no attribute 'deploy'
6. Loop or filter producing an unexpected shape
A json_query, selectattr, or from_json returning a list when you index it like a dict (or vice-versa) yields a missing attribute.
- set_fact:
primary: "{{ servers | selectattr('role','equalto','primary') | first }}"
- debug:
msg: "{{ primary.ip }}" # 'first' on empty selection -> undefined
'dict object' has no attribute 'ip'
Diagnostic Workflow
Step 1: Read the file, line, and column from the error
The traceback names the exact task file and line:
The error appears to be in '/home/deploy/site/roles/app/tasks/main.yml': line 22, column 7
Open that line — the undefined reference is in the {{ }} on or just above it.
Step 2: Dump what the variable actually contains
ansible web-01 -i inventory.ini -m debug -a "var=rel"
web-01 | SUCCESS => {
"rel": {
"changed": false,
"skipped": true,
"skip_reason": "Conditional result was False"
}
}
This immediately shows that rel.stdout does not exist because the task was skipped.
Step 3: Confirm the variable name and scope
ansible web-01 -i inventory.ini -m debug -a "var=hostvars[inventory_hostname].keys()|list" 2>/dev/null
ansible web-02 -i inventory.ini -m debug -a "var=db_host"
VARIABLE IS NOT DEFINED! confirms a scope/host problem.
Step 4: Check facts if it is an ansible_* variable
ansible web-01 -i inventory.ini -m setup -a 'filter=ansible_default_ipv4'
If empty, ensure gather_facts: true (or run setup) before the task that uses the fact.
Step 5: Guard the reference, then re-run
Make the access safe with a default and prove the play passes:
- debug:
msg: "{{ rel.stdout | default('n/a') }}"
when: rel.stdout is defined
ansible-playbook -i inventory.ini site.yml --limit web-01
Example Root Cause Analysis
A role that templates an nginx config works on staging but fails on one production host:
fatal: [web-03]: FAILED! => {"msg": "AnsibleUndefined: 'dict object' has no attribute 'address'. The error appears to be in '.../templates/nginx.conf.j2'"}
The template uses {{ ansible_default_ipv4.address }}. Dumping the fact on the failing host:
ansible web-03 -i inventory.ini -m setup -a 'filter=ansible_default_ipv4'
web-03 | SUCCESS => {
"ansible_facts": {
"ansible_default_ipv4": {}
}
}
ansible_default_ipv4 is an empty dict on web-03. This host has no default IPv4 route (it is dual-homed with only static routes), so Ansible could not populate the fact, and .address does not exist.
Fix: reference an explicit interface fact and provide a fallback, so a host without a default route still renders:
listen {{ (ansible_default_ipv4.address | default(ansible_eth0.ipv4.address)) }}:80;
The template now renders on web-03, and the play completes.
Prevention Best Practices
- Define sensible defaults in
roles/<role>/defaults/main.ymlso a missing override degrades gracefully instead of raising undefined. - Use the
default()filter andis definedguards on any value that depends on facts, registers, or per-host overrides:{{ x | default('') }}. - Run
ansible-lintin CI — it catches many undefined-variable and bad-reference patterns before they hit a host. - Keep
gather_facts: true(or runsetup) before any task that uses anansible_*fact, and never assume a fact is non-empty on every host. - Test playbooks with
--checkand across the full inventory, not just one host, so host-specific scope gaps surface early. - When triaging a wall of Jinja errors, the free incident assistant can map the traceback to the missing variable. See more in the Ansible guides.
Quick Command Reference
# Dump exactly what a variable contains
ansible <host> -i inventory.ini -m debug -a "var=<name>"
# Is the var defined on this host at all?
ansible <host> -i inventory.ini -m debug -a "var=<name>" # -> VARIABLE IS NOT DEFINED!
# Inspect a fact
ansible <host> -i inventory.ini -m setup -a 'filter=ansible_default_ipv4'
# See the rendered template / full traceback
ansible-playbook -i inventory.ini site.yml --limit <host> -vvv
# Safe reference patterns (in YAML)
# {{ var | default('fallback') }}
# when: var is defined
Conclusion
'dict object' has no attribute means a Jinja2 reference resolved to nothing. The traceback’s file/line and a quick debug -a "var=..." almost always pinpoint it. The usual root causes:
- A typo in the variable or attribute name.
- A registered result accessed after the task was skipped or without the subkey.
- A fact referenced with gathering disabled or filtered out.
- A variable defined in the wrong scope or only on some hosts.
- A nested key on a dict that is empty/missing on certain hosts.
- A filter or loop returning a different shape than you indexed.
Dump the variable, confirm its real name and scope, and guard the access with default()/is defined — that converts a hard failure into a predictable, debuggable value.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.