Understanding the Error
You’re running a playbook to deploy 15 Nginx virtual hosts, and everything seems fine until the task hits a wall. Ansible stops cold and throws this specific error:
Invalid data passed to 'loop', it requires a list, got <class 'dict'>
Ansible is being literal here. The loop keyword, which became the standard in version 2.5, strictly requires a YAML list (an array). If you pass it a dictionary—a set of key-value pairs wrapped in curly braces—the task will fail instantly. It’s like trying to put a square peg in a round hole.
Why This Happens
Most of the time, this snag happens because of how data is structured or captured. Here are the three most common culprits:
- Direct Dictionary Usage: You defined a variable as a dictionary but tried to loop over it as if it were a simple list.
- Registered Variables: You captured the output of a module like
find, but you're trying to loop over the entire result object instead of the list of files inside it. - YAML Syntax Slips: A missing dash (
-) in your variable file can accidentally turn a list into a dictionary.
Step-by-Step Fixes
Scenario 1: Converting a Dictionary on the Fly
Imagine you have a list of users defined in vars/main.yml like this:
# vars/main.yml
users:
alice:
uid: 1001
shell: /bin/bash
bob:
uid: 1002
shell: /bin/zsh
If you try to use loop: "{{ users }}", Ansible sees a dictionary and fails. The fix is the dict2items filter. It reshapes your dictionary into a list that Ansible can digest.
# The Corrected Task
- name: Create users from a dictionary
ansible.builtin.user:
name: "{{ item.key }}"
uid: "{{ item.value.uid }}"
shell: "{{ item.value.shell }}"
loop: "{{ users | dict2items }}"
Scenario 2: Navigating Registered Variables
This is a classic trap when using the find or stat modules. These modules return a massive dictionary containing metadata, timestamps, and status codes. The actual list you want is usually buried one level deeper.
# The WRONG way
- name: Find old log files
ansible.builtin.find:
paths: /var/log/nginx
patterns: "*.log.gz"
register: found_logs
- name: Cleanup logs
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ found_logs }}" # This fails!
The Fix: You must point the loop to the files attribute. That is where the actual list lives.
# The RIGHT way
- name: Cleanup logs
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ found_logs.files }}" # Target the specific list
Scenario 3: Spotting YAML Formatting Errors
YAML is sensitive. If you forget a dash, your data structure changes entirely. Take a look at this comparison:
# Incorrect: This is a Dictionary
my_packages:
git: present
vim: present
# Correct: This is a List
my_packages:
- git
- vim
Check your variable files. If your loop points to my_packages, ensure every item starts with a dash. Without those dashes, Ansible sees a dictionary and triggers the error.
Verification Steps
Unsure what data type you're dealing with? Use the debug module and the type_debug filter to inspect your variables mid-run. It saves a lot of guesswork.
- name: Check the data type
ansible.builtin.debug:
msg: "Variable type is {{ users | type_debug }}"
- name: Verify the conversion
ansible.builtin.debug:
msg: "After filter, type is {{ (users | dict2items) | type_debug }}"
If the output says list, you are good to go.
Practical Tips for Success
Visualizing YAML can be tough as playbooks grow. When I'm stuck, I use a YAML to JSON converter. JSON is much more rigid with its brackets and braces, making it obvious whether you’ve created an Object (Dictionary) or an Array (List).
One final trick: run your playbook with -v. Ansible will print the full JSON structure of your registered variables. This lets you see exactly which key holds the list you need for your loop without having to guess the attribute name.

