Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Ansible By James Joyner IV · · 9 min read

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 attribute and 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 -vvv shows 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.yml so a missing override degrades gracefully instead of raising undefined.
  • Use the default() filter and is defined guards on any value that depends on facts, registers, or per-host overrides: {{ x | default('') }}.
  • Run ansible-lint in CI — it catches many undefined-variable and bad-reference patterns before they hit a host.
  • Keep gather_facts: true (or run setup) before any task that uses an ansible_* fact, and never assume a fact is non-empty on every host.
  • Test playbooks with --check and 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:

  1. A typo in the variable or attribute name.
  2. A registered result accessed after the task was skipped or without the subkey.
  3. A fact referenced with gathering disabled or filtered out.
  4. A variable defined in the wrong scope or only on some hosts.
  5. A nested key on a dict that is empty/missing on certain hosts.
  6. 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.

Free download · 368-page PDF

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.