It all started with a question about Ansible - someone was getting a False when they were expecting a True after converting a variable using the bool filter. The solution to that particular problem was fairly easily found, but one additional detail caused me to go down the proverbial rabbit hole.

The initial question was around the fact that a variable containing ["True"] was evaluating to False. You might quickly notice that the value is a list and the conversion should be applied to the first element of that list, not to the list itself. Quick fix and now the result is as expected.

- name: WHAT IS TRUTH?
  gather_facts: false
  hosts: localhost

    b: ["True"]

    - debug:
        var: b | bool

    - debug:
        var: b | first | bool
TASK [debug] *************************************************************************
ok: [localhost] => {
    "b | bool": false

TASK [debug] *************************************************************************
ok: [localhost] => {
    "b | first | bool": true

But why?

So far so good - but the eyebrow raiser was this: why is a non-empty list evaluating as False? After all Ansible is written in Python. And in Python, almost everything is True, with a few exceptions like 0, None, empty sequences - read the docs.

Python 3.6.8 (default, Jan 14 2019, 11:02:34) 

>>> bool(["True"])

The answer is in the code, the Ansible code. Filters are functions and the bool filter is found in the plugins/filter/ file in Ansible 2.8. I've included it below for convenience.

def to_bool(a):
    ''' return a bool for the arg '''
    if a is None or isinstance(a, bool):
        return a
    if isinstance(a, string_types):
        a = a.lower()
    if a in ('yes', 'on', '1', 'true', 1):
        return True
    return False

It's immediately obvious that this filter does not use the Python conversion to bool, but rather implements its own logic to allow for YAML accepted values, but most importantly return False by default. This explains why trying to convert the list ["True"] to a boolean value yielded False instead of the (expected by me) True.

Trying to find out why this decision was made, I dug around the Ansible repository commit history, asked around, and even tried to search through past issues. Since this filter has been around since 2013, even Ansible's creator doesn't remember why exactly, apart from the initial reasoning that Ansible wasn't meant to require understanding of Python - so (my interpretation) is that its behaviour should not necessarily be in sync with Python's.

The way forward

There's also a function boolean() in module_utils that, by default, validates against a list of acceptable values and yells at you otherwise (in strict mode). This, to me, is a sane middle ground if one doesn't like the pure Python rules.

BOOLEANS_TRUE = frozenset(('y', 'yes', 'on', '1', 'true', 't', 1, 1.0, True))
BOOLEANS_FALSE = frozenset(('n', 'no', 'off', '0', 'false', 'f', 0, 0.0, False))

Both Python's bool() conversion and the module_utils boolean() are in use as shown by a quick and dirty rgrep through the Ansible code. Even more, I found a merged PR for Ansible 2.10 that will add truthy and falsy checks that are basically a front-end to the boolean() function.

Mind you, boolean() without strict mode will also default to False for any other values. That should ensure backwards compatibility and perhaps exist as a flag, with the bool filter using it by default with strict mode.

Why strict mode by default? Well, the issue that started this whole investigation is the best example: if ["True"] is not evaluated as per Python's rules, then it's better to yell that a list is not a valid boolean value and leave it at that. ["True"] evaluating to False is just as "fun" to investigate as ["False"] evaluating to True in the end.

Predictability and ease of troubleshooting win the day in my book any time.

And, as always, thanks for reading.

Any comments? Contact me via Mastodon or e-mail.

Share & Subscribe!