# How to merge several dicts

**URL:** <https://forum.ansible.com/t/how-to-merge-several-dicts/45378>\
**Category:** Get Help\
**Tags:** variables, role, ansible-core, howto\
**Created:** [February 23, 2026, 8:59am UTC](https://forum.ansible.com/t/how-to-merge-several-dicts/45378 "2026-02-23T08:59:03Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![kks](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/kks/32/10352_2.png) [@kks](https://forum.ansible.com/u/kks)\
**Post date:** [February 23, 2026, 8:59am UTC](https://forum.ansible.com/t/how-to-merge-several-dicts/45378/1 "2026-02-23T08:59:03Z")

</div>

This is continuation from discussion started with @utoddl in topic [Cannot change a variable passed to a role from within a role - request for docs - #11 by utoddl](https://forum.ansible.com/t/cannot-change-a-variable-passed-to-a-role-from-within-a-role-request-for-docs/45368/11)

The premise is following - there is a role that takes a dictionary as one of the parameters. And with dictionary there is not merging of parameters that are set on different levels - preceding values just replace values with lower precedence level as per [Using variables — Ansible Community Documentation](https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable)

Question is how to merge configuration dict that is passed to the role from dicts that are set on different levels. Here is a list of levels I came up with (from least important to most important)

- role defaults
- host group
- host

There might be several host groups - for instance group that contains the host, group that contains that group, group that contains that group, etc. That complication is not needed for example I would like to present, it can be easily extended.

@utoddl proposed following code

```yaml
- name: Populate splunk inputs.conf
  become: true
  become_user: splunk
  ansible.builtin.template:
    src: '{{ splunk_forwarder_inputsconf }}'
    dest: '{{ splunk_forwarder_homedir }}/etc/system/local/inputs.conf'
    owner: splunk
    group: splunk
    mode: 0644
  notify: restart splunkforwarder
  vars:
    # 'conf_var_names' contains a list of all the variables starting with
    # "splunk_forwarder_inputs_conf_" except "…defaults".
    conf_var_names: "{{ [ [lookup('ansible.builtin.varnames',
                                 '^splunk_forwarder_inputs_conf_(?!defaults$).*', default='')]
                        ] | flatten
                        | map('split', ',')
                        | flatten }}"
    # Combine the 'splunk_forwarder_inputs_conf_defaults' role default
    # and any host- or group-specific inputs_conf variables that might
    # apply to the current play host.
    conf_var_values: "{{ splunk_forwarder_inputs_conf_defaults
                        | combine(lookup('ansible.builtin.vars', *conf_var_names)) }}"

```

I see that there are few improvements that can be made to that code. Here are the issues I would like to solve

1. Lookup ``lookup(‘ansible.builtin.varnames’,￼‘^splunk_forwarder_inputs_conf_(?!defaults$).*’, default=‘’` does not guarantee order in which variable names will be returned, so when we combine them with `combine(lookup('ansible.builtin.vars', *conf_var_names))` result might differ due to order being different
2. Lookup ``lookup(‘ansible.builtin.varnames’,￼‘^splunk_forwarder_inputs_conf_(?!defaults$).*’, default=‘’` return all the variables which name starts with `splunk_forwarder_inputs_conf_` so variable named `splunk_forwarder_inputs_conf_whatever` will be found and merged again providing incosistent results

I came up with the following code

```yaml
ocalhost
  gather_facts: false
  vars:
    configuration_variable: "splunk_forwarder_inputs_conf_"
    suffixes:
      - default
      - host_group
      - host
    configuration_variable_all_existing_variables: "{{ [lookup('ansible.builtin.varnames', '^'~configuration_variable~'.*', default='')] | map('split',',') | flatten }}"
    configuration_variable_list_of_variable_names_with_suffixes: "{{ suffixes | map('regex_replace', '^', configuration_variable) }}"
    configuration_variable_list_of_variable_names_only_existing: "{{ configuration_variable_list_of_variable_names_with_suffixes | select('in', configuration_variable_all_existing_variables) }}"
    configuration_variable_dict: "{{ {} | combine(lookup('ansible.builtin.vars', *configuration_variable_list_of_variable_names_only_existing)) }}"
    splunk_forwarder_inputs_conf_default:
      option1: default
      option2: default
      option3: default
    splunk_forwarder_inputs_conf_host:
      option2: host
    splunk_forwarder_inputs_conf_host_group:
      option2: host_group
      option3: host_group
    splunk_forwarder_inputs_conf_doesnot_do_anything:
      option1: fail
      option2: fail
      option3: fail

  tasks:
    - name: Print all existing variable with name as prefix
      ansible.builtin.debug:
        var: configuration_variable_all_existing_variables

    - name: Print all suffixed variables
      ansible.builtin.debug:
        var: configuration_variable_list_of_variable_names_with_suffixes

    - name: Print configuration variable resulting dict
      ansible.builtin.debug:
        var: configuration_variable_dict

    - name: Print configuration variable resulting dict, but the order is different
      ansible.builtin.debug:
        var: configuration_variable_dict
      vars:
        suffixes:
          - default
          - host
          -

```

Ideas is the same as in the code from @utoddl, but here we control what suffixed will be used to construct dictionaries that will be merged - this is variable `suffixes` - order is very important in this list.

From suffixes list all the variables that can influence the result are condsructed in variable `configuration_variable_list_of_variable_names_with_suffixes`

Only existing variables when selected for combining - `configuration_variable_list_of_variable_names_only_existing` - in this list there are variables that can influence the result.

And when all the variables are combined in the order of the suffix list, I start combining from empty dict, default goes first and so on.

This works starting with ansible-core 2.13

> **uv run --with ansible-core==2.13 ansible-playbook merge\_with\_list.yml**
>
> bash-5.2$ uv run --with ansible-core==2.13 ansible-playbook merge\_with\_list.yml  
> [WARNING]: No inventory was parsed, only implicit localhost is available  
> [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match ‘all’
> 
> PLAY [localhost] \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
> 
> TASK [Print all existing variable with name as prefix] \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*  
> ok: [localhost] =\> {  
> “configuration\_variable\_all\_existing\_variables”: [  
> “splunk\_forwarder\_inputs\_conf\_default”,  
> “splunk\_forwarder\_inputs\_conf\_host”,  
> “splunk\_forwarder\_inputs\_conf\_host\_group”,  
> “splunk\_forwarder\_inputs\_conf\_doesnot\_do\_anything”  
> ]  
> }
> 
> TASK [Print all suffixed variables] \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*  
> ok: [localhost] =\> {  
> “configuration\_variable\_list\_of\_variable\_names\_with\_suffixes”: [  
> “splunk\_forwarder\_inputs\_conf\_default”,  
> “splunk\_forwarder\_inputs\_conf\_host\_group”,  
> “splunk\_forwarder\_inputs\_conf\_host”  
> ]  
> }
> 
> TASK [Print configuration variable resulting dict] \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*  
> ok: [localhost] =\> {  
> “configuration\_variable\_dict”: {  
> “option1”: “default”,  
> “option2”: “host”,  
> “option3”: “host\_group”  
> }  
> }
> 
> TASK [Print configuration variable resulting dict, but the order is different] \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*  
> ok: [localhost] =\> {  
> “configuration\_variable\_dict”: {  
> “option1”: “default”,  
> “option2”: “host\_group”,  
> “option3”: “host\_group”  
> }  
> }
> 
> PLAY RECAP \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*  
> localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

> **uv run --with ansible-core==2.20 ansible-playbook merge\_with\_list.yml**
>
> bash-5.2$ uv run --with ansible-core==2.20 ansible-playbook merge\_with\_list.yml  
> [WARNING]: No inventory was parsed, only implicit localhost is available  
> [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match ‘all’
> 
> PLAY [localhost] \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
> 
> TASK [Print all existing variable with name as prefix] \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*  
> ok: [localhost] =\> {  
> “configuration\_variable\_all\_existing\_variables”: [  
> “splunk\_forwarder\_inputs\_conf\_default”,  
> “splunk\_forwarder\_inputs\_conf\_host”,  
> “splunk\_forwarder\_inputs\_conf\_host\_group”,  
> “splunk\_forwarder\_inputs\_conf\_doesnot\_do\_anything”  
> ]  
> }
> 
> TASK [Print all suffixed variables] \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*  
> ok: [localhost] =\> {  
> “configuration\_variable\_list\_of\_variable\_names\_with\_suffixes”: [  
> “splunk\_forwarder\_inputs\_conf\_default”,  
> “splunk\_forwarder\_inputs\_conf\_host\_group”,  
> “splunk\_forwarder\_inputs\_conf\_host”  
> ]  
> }
> 
> TASK [Print configuration variable resulting dict] \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*  
> ok: [localhost] =\> {  
> “configuration\_variable\_dict”: {  
> “option1”: “default”,  
> “option2”: “host”,  
> “option3”: “host\_group”  
> }  
> }
> 
> TASK [Print configuration variable resulting dict, but the order is different] \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*  
> ok: [localhost] =\> {  
> “configuration\_variable\_dict”: {  
> “option1”: “default”,  
> “option2”: “host\_group”,  
> “option3”: “host\_group”  
> }  
> }
> 
> PLAY RECAP \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*  
> localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

I hope this is helpful
