Skip to main content

How to work with a list of dictionaries in Ansible

Use Jinja selectattr() and map() filters to extract data from a list of dictionaries.

My friend and peer Roberto Nozaki recently wrote How to work with lists and dictionaries in Ansible. After reading it, this felt like a perfect opportunity to follow up with some related content. If you're unsure of what dictionaries or lists are in Ansible, give his article a quick read and then come back here. Otherwise, keep reading, as I'm confident you'll find it worthwhile if you're new to using Jinja filtering.

[ Get started with IT automation with the Ansible Automation Platform beginner's guide. ]

The Jinja go-to filters

When you're working with Ansible, you typically work with lists and dictionaries. On occasion, the data you're working with is actually a list of dictionaries. This is where Jinja filtering comes in handy.

Jinja is powerful templating language that has two filters that are ideal for extracting data from lists of dictionaries. They are: selectattr() and map().

The Jinja documentation explains the filters like so:

  • selectattr() filters a sequence of objects by applying a test to the specified attribute of each object and only selecting the objects when the test succeeds.
  • map() applies a filter on a sequence of objects or looks up an attribute. This is useful when you have lists of objects but you are interested in only a certain value.

Learning how to use these filters properly will take your Ansible skills to the next level. To be fair, there are a lot of Jinja filters. In this article, I focus on selectattr() and map().

While some Ansible users prefer to use the json_query filter, I personally prefer to use Jinja's selectattr() and map() filters. Ansible natively supports Jinja. Since selectattr() and map() are Jinja filters, you don't need to add anything to your server if Ansible is installed.

The json_query module, however, is part of the community.general collection. Thus, it is not natively installed with Ansible. You may not have the luxury of adding collections on the fly without some level of approval beforehand. Using Jinja filters mitigates the immediate need for external collections to extract data from lists of dictionaries.

[ Download now: A system administrator's guide to IT automation. ]

Get started with filters

First, create a variable called bands. The variable bands is a list of dictionaries that will be used in the example.yml playbook. As you put the playbook together, you'll begin to see how to select data from the variable.

# List of dictionaries
      bands:
        - name: The Beatles
          members:
          - Lennon
          - McCartney
          - Harrison
          - Starr
          formed: 1960
          decade: 60s

        - name: The Eagles
          members:
          - Frey
          - Henley
          - Leadon
          - Meisner
          formed: 1971
          decade: 70s

        - name: Run DMC
          members:
          - Simmons
          - McDaniels
          - Mizell
          formed: 1983
          decade: 80s

        - name: Red Hot Chili Peppers
          members:
          - Kiedis
          - Smith
          - Frusciante
          - Balzary
          formed: 1982
          decade: 90s

        - name: "Destiny's Child"
          members:
          - Knowles
          - Rowland
          - Williams
          formed: 1990
          decade: 00s

        - name: "Black Eyed Peas"
          members:
          - Adams
          - Lindo
          - Gomez
          formed: 1995
          decade: 00s
...

Next, create a set of tasks that show what kind of data you are dealing with. In the first task, you should see that the bands variable itself is a list. The second task should show the bands variable is made up of dictionaries. Simply put, this shows you're dealing with a list of dictionaries.

    - name: Show what type of variable bands is
      ansible.builtin.debug:
        msg: "{{ bands | type_debug }}"

    - name: Show what type of data the bands variable is made up of
      ansible.builtin.debug:
        msg: "{{ item | type_debug }}"
      loop: "{{ bands }}"

For the next task, list all members of all bands. You're selecting the members key of the dictionary if the members key is "defined." In this case you know it is, so you can expect data to be returned. Once you select the data, pass it to the map filter. The map filter applies a filter on a sequence of objects or looks up an attribute. In this case, you're looking up the members attribute because that's the data you're interested in:

    - name: List ALL band members of ALL bands
      ansible.builtin.debug:
        msg: "{{ bands | selectattr('members', 'defined') | map(attribute='members') }}"

Say you're interested in when the bands were formed rather than who the members are. To do so, create a task to find the years that the bands were formed, regardless of other key/value pairs:

    - name: List the years the bands were formed
      ansible.builtin.debug:
          msg: "{{ bands | selectattr('formed', 'defined') | map(attribute='formed') }}"

Or maybe you want to know the members of a band that formed in a specific year. In this task, you're looking for the members of the band that was formed in the year 1971:

     - name: List members of band formed in 1971
       ansible.builtin.debug:
            msg: "{{ bands | selectattr('formed', 'match', '1971') | map(attribute='members') }}"

For the next task, find the name of a band that has a member named "Starr." When doing searches like this, the search/match parameters are case sensitive. For this example, you're specifically looking for the name of the band in which "Starr" was a member:

    - name: List band formed with a member named Starr
      ansible.builtin.debug:
          msg: "{{ bands | selectattr('members', 'search', 'Starr') | map(attribute='name') }}"

What if more than one list of dictionaries has the same data? No problem! You can grab as much or as little as you wish. The first example below grabs all decades that start with a single character followed by "0" and ending with "s." The second example grabs all band names that have "00" in the "decade" value.

      - name: List band name when band decade ends with 0s regardless of the decade
        ansible.builtin.debug:
            msg: "{{ bands | selectattr('decade', 'search', '^.0s$') | map(attribute='name') }}"

      - name: List band name when band decade contains 00 in the name
        ansible.builtin.debug:
            msg: "{{ bands | selectattr('decade', 'search', '00') | map(attribute='name') }}"

[ Write your first Ansible playbook in this hands-on interactive lab. ]

Put everything together in a playbook

Now that the tasks are defined, put it altogether in a single playbook:

---
- name: 'Jinja selectattr() and map() example with list of dictionaries'
  hosts: localhost
  gather_facts: false
  vars:
  # This is a list of dictionaries
      bands:
          - name: The Beatles
            members:
                - Lennon
                - McCartney
                - Harrison
                - Starr
            formed: 1960
            decade: 60s

          - name: The Eagles
            members:
                - Frey
                - Henley
                - Leadon
                - Meisner
            formed: 1971
            decade: 70s

          - name: Run DMC
            members:
                - Simmons
                - McDaniels
                - Mizell
            formed: 1983
            decade: 80s

          - name: Red Hot Chili Peppers
            members:
                - Kiedis
                - Smith
                - Frusciante
                - Balzary
            formed: 1982
            decade: 90s

          - name: "Destiny's Child"
            members:
                - Knowles
                - Rowland
                - Williams
            formed: 1990
            decade: 00s

          - name: "Black Eyed Peas"
            members:
                - Adams
                - Lindo
                - Gomez
            formed: 1995
            decade: 00s

  tasks:

      - name: Show what type of variable bands be
        ansible.builtin.debug:
            msg: "{{ bands | type_debug }}"

      - name: Show what type of data the bands variable consists of
        ansible.builtin.debug:
            msg: "{{ item | type_debug }}"
        loop: "{{ bands }}"

      - name: List band members of all bands
        ansible.builtin.debug:
            msg: "{{ bands | selectattr('members', 'defined') | map(attribute='members') }}"

      - name: List the years the bands were formed
        ansible.builtin.debug:
            msg: "{{ bands | selectattr('formed', 'defined') | map(attribute='formed') }}"

      - name: List members of band formed in 1971
        ansible.builtin.debug:
            msg: "{{ bands | selectattr('formed', 'match', '1971') | map(attribute='members') }}"

      - name: List band with a member named Starr
        ansible.builtin.debug:
            msg: "{{ bands | selectattr('members', 'search', 'Starr') | map(attribute='name') }}"

      - name: List band name when band decade ends with 0s regardless of the decade
        ansible.builtin.debug:
            msg: "{{ bands | selectattr('decade', 'search', '^.0s$') | map(attribute='name') }}"

      - name: List band name when band decade contains 00 in the name
        ansible.builtin.debug:
            msg: "{{ bands | selectattr('decade', 'search', '00') | map(attribute='name') }}"

      - name: Loop through the the 80s and 90s bands and display a message if Run DMC is in the name
        ansible.builtin.debug:
            msg:
                - !unsafe It's Tricky to rock a rhyme
        loop: "{{ bands | selectattr('decade', 'search', '^[8-9]0s$') | map(attribute='name') }}"
        when: "'run' in item | lower"
...

Execute the playbook and check the results

Execute the playbook to see the filters in action.

Note that I performed this test on a Fedora 37 host with Ansible 7.3. Be sure your version of Jinja supports the arguments you're using for selectattr() if you want to replicate this exercise in your own environment.

$ ansible-playbook example.yml 

PLAY [Jinja selectattr() and map() example with list of dictionaries] *********

TASK [Show what type of variable bands be] ************************************
ok: [localhost] => {
    "msg": "list"
}

TASK [Show what type of data the bands variable consists of] ******************
ok: [localhost] => (item={'name': 'The Beatles', 'members': ['Lennon', 'McCartney', 'Harrison', 'Starr'], 'formed': 1960, 'decade': '60s'}) => {
    "msg": "dict"
}
ok: [localhost] => (item={'name': 'The Eagles', 'members': ['Frey', 'Henley', 'Leadon', 'Meisner'], 'formed': 1971, 'decade': '70s'}) => {
    "msg": "dict"
}
ok: [localhost] => (item={'name': 'Run DMC', 'members': ['Simmons', 'McDaniels', 'Mizell'], 'formed': 1983, 'decade': '80s'}) => {
    "msg": "dict"
}
ok: [localhost] => (item={'name': 'Red Hot Chili Peppers', 'members': ['Kiedis', 'Smith', 'Frusciante', 'Balzary'], 'formed': 1982, 'decade': '90s'}) => {
    "msg": "dict"
}
ok: [localhost] => (item={'name': "Destiny's Child", 'members': ['Knowles', 'Rowland', 'Williams'], 'formed': 1990, 'decade': '00s'}) => {
    "msg": "dict"
}
ok: [localhost] => (item={'name': 'Black Eyed Peas', 'members': ['Adams', 'Lindo', 'Gomez'], 'formed': 1995, 'decade': '00s'}) => {
    "msg": "dict"
}

TASK [List band members of all bands] *****************************************
ok: [localhost] => {
    "msg": [
        [
            "Lennon",
            "McCartney",
            "Harrison",
            "Starr"
        ],
        [
            "Frey",
            "Henley",
            "Leadon",
            "Meisner"
        ],
        [
            "Simmons",
            "McDaniels",
            "Mizell"
        ],
        [
            "Kiedis",
            "Smith",
            "Frusciante",
            "Balzary"
        ],
        [
            "Knowles",
            "Rowland",
            "Williams"
        ],
        [
            "Adams",
            "Lindo",
            "Gomez"
        ]
    ]
}

TASK [List the years the bands were formed] ***********************************
ok: [localhost] => {
    "msg": [
        1960,
        1971,
        1983,
        1982,
        1990,
        1995
    ]
}

TASK [List members of band formed in 1971] ************************************
ok: [localhost] => {
    "msg": [
        [
            "Frey",
            "Henley",
            "Leadon",
            "Meisner"
        ]
    ]
}

TASK [T08 - List band with a member named Starr] ******************************
ok: [localhost] => {
    "msg": [
        "The Beatles"
    ]
}

TASK [List band name when band decade ends with 0s regardless of the decade] **
ok: [localhost] => {
    "msg": [
        "The Beatles",
        "The Eagles",
        "Run DMC",
        "Red Hot Chili Peppers",
        "Destiny's Child",
        "Black Eyed Peas"
    ]
}

TASK [List band name when band decade contains 00 in the name] ****************
ok: [localhost] => {
    "msg": [
        "Destiny's Child",
        "Black Eyed Peas"
    ]
}

TTASK [Loop through the the 80s and 90s bands and display a message if Run DMC is in the name] *****
ok: [localhost] => (item=Run DMC) => {
    "msg": [
        "It's Tricky.....to rock a rhyme"
    ]
}
skipping: [localhost] => (item=Red Hot Chili Peppers) 

PLAY RECAP ********************************************************************
localhost   : ok=9    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Wrap up

Manipulating lists of dictionaries is a critical skill when automating tasks with Ansible or Red Hat Ansible Automation Platform (AAP), but it can be intimidating when you're first learning how to use it.

Copy this example and practice using Jinja filtering as part of your skills development. Hopefully this article gave you enough of a glimpse to increase your interest in using Jinja filtering.

Topics:   Ansible   Automation  

Randy Romero

Randy Romero is a Senior Ansible Consultant and Red Hat Certified Architect at Red Hat where he specializes in IT automation with Ansible and Ansible Automation Platform.  He has experience in the cable, insurance, and loyalty-marketing industries, having performed multiple roles starting with ju More about me

Try Red Hat Enterprise Linux

Download it at no charge from the Red Hat Developer program.