TL;DR? It’s possible to work out what type of variable you’re working with in Ansible. The in-built filters don’t always do quite what you’re expecting. Jump to the “In Summary” heading for my suggestions.
LATE EDIT: 2021-05-23 After raising a question in #ansible on Freenode, flowerysong noticed that my truth table around mappings, iterables and strings was wrong. I’ve amended the table accordingly, and have added a further note below the table.
One of the things I end up doing quite a bit with Ansible is value manipulation. I know it’s not really normal, but… well, I like rewriting values from one type of a thing to the next type of a thing.
For example, I like taking a value that I don’t know if it’s a list or a string, and passing that to an argument that expects a list.
Doing it wrong, getting it better
Until recently, I’d do that like this:
- debug:
    msg: |-
      {
        {%- if value | type_debug == "string" or value | type_debug == "AnsibleUnicode" -%}
           "string": "{{ value }}"
        {%- elif value | type_debug == "dict" or value | type_debug == "ansible_mapping" -%}
          "dict": {{ value }}
        {%- elif value | type_debug == "list" -%}
          "list": {{ value }}
        {%- else -%}
          "other": "{{ value }}"
        {%- endif -%}
      }But, following finding this gist, I now know I can do this:
- debug:
    msg: |-
      {
        {%- if value is string -%}
           "string": "{{ value }}"
        {%- elif value is mapping -%}
          "dict": {{ value }}
        {%- elif value is iterable -%}
          "list": {{ value }}
        {%- else -%}
          "other": "{{ value }}"
        {%- endif -%}
      }So, how would I use this, given the context of what I was saying before?
- assert:
    that:
    - value is string
    - value is not mapping
    - value is iterable
- some_module:
    some_arg: |-
      {%- if value is string -%}
        ["{{ value }}"]
      {%- else -%}
        {{ value }}
      {%- endif -%}More details on finding a type
Why in this order? Well, because of how values are stored in Ansible, the following states are true:
| ⬇️Type \ ➡️Check | is iterable | is mapping | is sequence | is string | 
|---|---|---|---|---|
| a_dict(e.g. {}) | ✔️ | ✔️ | ✔️ | ❌ | 
| a_list(e.g. []) | ✔️ | ❌ | ✔️ | ❌ | 
| a_string(e.g. “”) | ✔️ | ❌ | ✔️ | ✔️ | 
So, if you were to check for is iterable first, you might match on a_list or a_dict instead of a_string, but string can only match on a_string. Once you know it can’t be a string, you can check whether something is mapping – again, because a mapping can only match a_dict, but it can’t match a_list or a_string. Once you know it’s not that, you can check for either is iterable or is sequence because both of these match a_string, a_dict and a_list.
LATE EDIT: 2021-05-23 Note that a prior revision of this table and it’s following paragraph showed “is_mapping” as true for a_string. This is not correct, and has been fixed, both in the table and the paragraph.
Likewise, if you wanted to check whether a_float and an_integer is number and not is string, you can check these:
| ⬇️Type \ ➡️Check | is float | is integer | is iterable | is mapping | is number | is sequence | is string | 
|---|---|---|---|---|---|---|---|
| a_float | ✔️ | ❌ | ❌ | ❌ | ✔️ | ❌ | ❌ | 
| an_integer | ❌ | ✔️ | ❌ | ❌ | ✔️ | ❌ | ❌ | 
So again, a_float and an_integer don’t match is string, is mapping or is iterable, but they both match is number and they each match their respective is float and is integer checks.
How about each of those (a_float and an_integer) wrapped in quotes, making them a string? What happens then?
| ⬇️Type \ ➡️Check | is float | is integer | is iterable | is mapping | is number | is sequence | is string | 
|---|---|---|---|---|---|---|---|
| a_float_as_string | ❌ | ❌ | ✔️ | ❌ | ❌ | ✔️ | ✔️ | 
| an_integer_as_string | ❌ | ❌ | ✔️ | ❌ | ❌ | ✔️ | ✔️ | 
This is somewhat interesting, because they look like a number, but they’re actually “just” a string. So, now you need to do some comparisons to make them look like numbers again to check if they’re numbers.
Changing the type of a string
What happens if you cast the values? Casting means to convert from one type of value (e.g. string) into another (e.g. float) and to do that, Ansible has three filters we can use, float, int and string. You can’t cast to a dict or a list, but you can use dict2items and items2dict (more on those later). So let’s start with casting our group of a_ and an_ items from above. Here’s a list of values I want to use:
---
- hosts: localhost
  gather_facts: no
  vars:
    an_int: 1
    a_float: 1.1
    a_string: "string"
    an_int_as_string: "1"
    a_float_as_string: "1.1"
    a_list:
      - item1
    a_dict:
      key1: value1With each of these values, I returned the value as Ansible knows it, what happens when you do {{ value | float }} to cast it as a float, as an integer by doing {{ value | int }} and as a string {{ value | string }}. Some of these results are interesting. Note that where you see u'some value' means that Python converted that string to a Unicode string.
| ⬇️Value \ ➡️Cast | value | value when cast as float | value when cast as integer | value when cast as string | 
|---|---|---|---|---|
| a_dict | {“key1”: “value1”} | 0.0 | 0 | “{u’key1′: u’value1′}” | 
| a_float | 1.1 | 1.1 | 1 | “1.1” | 
| a_float_as_string | “1.1” | 1.1 | 1 | “1.1” | 
| a_list | [“item1”] | 0.0 | 0 | “[u’item1′]” | 
| a_string | “string” | 0.0 | 0 | “string” | 
| an_int | 1 | 1 | 1 | “1” | 
| an_int_as_string | “1” | 1 | 1 | “1” | 
So, what does this mean for us? Well, not a great deal, aside from to note that you can “force” a number to be a string, or a string which is “just” a number wrapped in quotes can be forced into being a number again.
Oh, and casting dicts to lists and back again? This one is actually pretty clearly documented in the current set of documentation (as at 2.9 at least!)
Checking for miscast values
How about if I want to know whether a value I think might be a float stored as a string, how can I check that?
{{ vars[var] | float | string == vars[var] | string }}
What is this? If I cast a value that I think might be a float, to a float, and then turn both the cast value and the original into a string, do they match? If I’ve got a string or an integer, then I’ll get a false, but if I have actually got a float, then I’ll get true. Likewise for casting an integer. Let’s see what that table looks like:
| ⬇️Type \ ➡️Check | value when cast as float | value when cast as integer | value when cast as string | 
|---|---|---|---|
| a_float | ✔️ | ❌ | ✔️ | 
| a_float_as_string | ✔️ | ❌ | ✔️ | 
| an_integer | ❌ | ✔️ | ✔️ | 
| an_integer_as_string | ❌ | ✔️ | ✔️ | 
So this shows us the values we were after – even if you’ve got a float (or an integer) stored as a string, by doing some careful casting, you can confirm they’re of the type you wanted… and then you can pass them through the right filter to use them in your playbooks!
Booleans
Last thing to check – boolean values – “True” or “False“. There’s a bit of confusion here, as a “boolean” can be: true or false, yes or no, 1 or 0, however, is true and True and TRUE the same? How about false, False and FALSE? Let’s take a look!
| ⬇️Value \ ➡️Check | type_debug | is boolean | is number | is iterable | is mapping | is string | value when cast as bool | value when cast as string | value when cast as integer | 
|---|---|---|---|---|---|---|---|---|---|
| yes | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | True | True | 1 | 
| Yes | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | Yes | 0 | 
| YES | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | YES | 0 | 
| “yes” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | yes | 0 | 
| “Yes” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | Yes | 0 | 
| “YES” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | YES | 0 | 
| true | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | True | True | 1 | 
| True | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | True | True | 1 | 
| TRUE | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | True | True | 1 | 
| “true” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | true | 0 | 
| “True” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | True | 0 | 
| “TRUE” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | TRUE | 0 | 
| 1 | int | ❌ | ✔️ | ❌ | ❌ | ❌ | True | 1 | 1 | 
| “1” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | 1 | 1 | 
| no | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | False | False | 0 | 
| No | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | False | False | 0 | 
| NO | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | False | False | 0 | 
| “no” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | no | 0 | 
| “No” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | No | 0 | 
| “NO” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | NO | 0 | 
| false | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | False | False | 0 | 
| False | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | False | False | 0 | 
| FALSE | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | False | False | 0 | 
| “false” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | false | 0 | 
| “False” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | False | 0 | 
| “FALSE” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | FALSE | 0 | 
| 0 | int | ❌ | ✔️ | ❌ | ❌ | ❌ | False | 0 | 0 | 
| “0” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | 0 | 0 | 
So, the stand out thing for me here is that while all the permutations of string values of the boolean representations (those wrapped in quotes, like this: "yes") are treated as strings, and shouldn’t be considered as “boolean” (unless you cast for it explicitly!), and all non-string versions of true, false,  and no are considered to be boolean, yes, Yes and YES are treated differently, depending on case. So, what would I do?
In summary
- Consistently use nooryes,trueorfalsein lower case to indicate a boolean value. Don’t use1or0unless you have to.
- If you’re checking that you’re working with a string, a list or a dict, check in the order string(usingis string),dict(usingis mapping) and thenlist(usingis sequenceoris iterable)
- Checking for numbers that are stored as strings? Cast your string through the type check for that number, like this: {% if value | float | string == value | string %}{{ value | float }}{% elif value | int | string == value | string %}{{ value | int }}{% else %}{{ value }}{% endif %}
- Try not to use type_debugunless you really can’t find any other way. These values will change between versions, and this caused me a lot of issues with a large codebase I was working on a while ago!
Run these tests yourself!
Want to run these tests yourself? Here’s the code I ran (also available in a Gist on GitHub), using Ansible 2.9.10.
---
- hosts: localhost
  gather_facts: no
  vars:
    an_int: 1
    a_float: 1.1
    a_string: "string"
    an_int_as_string: "1"
    a_float_as_string: "1.1"
    a_list:
      - item1
    a_dict:
      key1: value1
  tasks:
    - debug:
        msg: |
          {
          {% for var in ["an_int", "an_int_as_string","a_float", "a_float_as_string","a_string","a_list","a_dict"] %}
            "{{ var }}": {
              "type_debug": "{{ vars[var] | type_debug }}",
              "value": "{{ vars[var] }}",
              "is float": "{{ vars[var] is float }}",
              "is integer": "{{ vars[var] is integer }}",
              "is iterable": "{{ vars[var] is iterable }}",
              "is mapping": "{{ vars[var] is mapping }}",
              "is number": "{{ vars[var] is number }}",
              "is sequence": "{{ vars[var] is sequence }}",
              "is string": "{{ vars[var] is string }}",
              "value cast as float": "{{ vars[var] | float }}",
              "value cast as integer": "{{ vars[var] | int }}",
              "value cast as string": "{{ vars[var] | string }}",
              "is same when cast to float": "{{ vars[var] | float | string == vars[var] | string }}",
              "is same when cast to integer": "{{ vars[var] | int | string == vars[var] | string }}",
              "is same when cast to string": "{{ vars[var] | string == vars[var] | string }}",
            },
          {% endfor %}
          }---
- hosts: localhost
  gather_facts: false
  vars:
    # true, True, TRUE, "true", "True", "TRUE"
    a_true: true
    a_true_initial_caps: True
    a_true_caps: TRUE
    a_string_true: "true"
    a_string_true_initial_caps: "True"
    a_string_true_caps: "TRUE"
    # yes, Yes, YES, "yes", "Yes", "YES"
    a_yes: yes
    a_yes_initial_caps: Tes
    a_yes_caps: TES
    a_string_yes: "yes"
    a_string_yes_initial_caps: "Yes"
    a_string_yes_caps: "Yes"
    # 1, "1"
    a_1: 1
    a_string_1: "1"
    # false, False, FALSE, "false", "False", "FALSE"
    a_false: false
    a_false_initial_caps: False
    a_false_caps: FALSE
    a_string_false: "false"
    a_string_false_initial_caps: "False"
    a_string_false_caps: "FALSE"
    # no, No, NO, "no", "No", "NO"
    a_no: no
    a_no_initial_caps: No
    a_no_caps: NO
    a_string_no: "no"
    a_string_no_initial_caps: "No"
    a_string_no_caps: "NO"
    # 0, "0"
    a_0: 0
    a_string_0: "0"
  tasks:
    - debug:
        msg: |
          {
          {% for var in ["a_true","a_true_initial_caps","a_true_caps","a_string_true","a_string_true_initial_caps","a_string_true_caps","a_yes","a_yes_initial_caps","a_yes_caps","a_string_yes","a_string_yes_initial_caps","a_string_yes_caps","a_1","a_string_1","a_false","a_false_initial_caps","a_false_caps","a_string_false","a_string_false_initial_caps","a_string_false_caps","a_no","a_no_initial_caps","a_no_caps","a_string_no","a_string_no_initial_caps","a_string_no_caps","a_0","a_string_0"] %}
            "{{ var }}": {
              "type_debug": "{{ vars[var] | type_debug }}",
              "value": "{{ vars[var] }}",
              "is float": "{{ vars[var] is float }}",
              "is integer": "{{ vars[var] is integer }}",
              "is iterable": "{{ vars[var] is iterable }}",
              "is mapping": "{{ vars[var] is mapping }}",
              "is number": "{{ vars[var] is number }}",
              "is sequence": "{{ vars[var] is sequence }}",
              "is string": "{{ vars[var] is string }}",
              "is bool": "{{ vars[var] is boolean }}",
              "value cast as float": "{{ vars[var] | float }}",
              "value cast as integer": "{{ vars[var] | int }}",
              "value cast as string": "{{ vars[var] | string }}",
              "value cast as bool": "{{ vars[var] | bool }}",
              "is same when cast to float": "{{ vars[var] | float | string == vars[var] | string }}",
              "is same when cast to integer": "{{ vars[var] | int | string == vars[var] | string }}",
              "is same when cast to string": "{{ vars[var] | string == vars[var] | string }}",
              "is same when cast to bool": "{{ vars[var] | bool | string == vars[var] | string }}",
            },
          {% endfor %}
          }Featured image is “Kelvin Test” by “Eelke” on Flickr and is released under a CC-BY license.
 
	 
		
The awesome article!
Thanks :)