Skip to content

Boolean argument conversion does not convert all values to bool #5646

@pekkaklarck

Description

@pekkaklarck

Boolean argument conversion currently works like this:

  • Integers, floats, strings and None are accepted. Other values cause an error.
  • Integers, floats and None are passed as-is without conversion.
  • Strings that are case-insensitively considered true (True, 1, yes, etc. depending on language configuration) yield Boolean True.
  • Strings that are case-insensitively considered false (False, 0, no, ...) yield Boolean False.
  • String None, case-insensitively, yields actual None object.
  • Other strings are passed as-is without conversion.

This is rather surprising, because most values are not actually converted to Boolean values but instead passed as-is. Due to integers, floats, None and strings having sane "truth values", this doesn't matter much if arguments are used like if arg:. It is, however, strange that if you have a keyword like def example(arg: bool), the values that are passed in are not guaranteed to have bool type.

Even though the current behavior is rather strange, it works as it does by design. The main motivation was handling cases where the argument type is got based on the default value and where converting all values would have caused problems. For example, even standard libraries had keywords like below before type hints were added in RF 7.4 (#5373):

def should_be_equal(first, second, .., strip_spaces=False, ...):
    ...
    if strip_spaces == "LEADING":
        first = first.lstrip()
        second = second.lstrip()
    elif strip_spaces == "TRAILING":
        first = first.rstrip()
        second = second.rstrip()
    elif strip_spaces:
        first = first.strip()
        second = second.strip()
    ...

It is likely that there exist keywords like above still today and thus changing the Boolean conversion behavior when the type is got based on the default value is not a good idea. We could, however, change it if the type is specified explicitly like arg: bool. If you actually want to accept strings or None in addition to Boolean values, you can use arg: bool | str or arg: bool | None. If you only want to accept certain strings, using something like arg: bool | Literal["LEADING", "TRAILING"] is very convenient and that's what the standard libraries nowadays use.

My concrete proposal is this:

  1. If argument type is specified explicitly to be bool, convert values all accepted values either to True or False.
  2. If argument type is got based on the default value, preserve the current behavior.
  3. Consider changing conversion so that in the explicit type case all values are accepted and converted to bool. This should perhaps get a separate issue.

Making this change is likely to require some changes to how explicitly (e.g. arg: bool) and implicitly (e.g. arg=True) got types are handled. That makes the change a bit bigger, but I expect the resulting code to be simpler than it is nowadays. We probably should implement #4881 as part of that change too.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions