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:
- If argument type is specified explicitly to be
bool, convert values all accepted values either to True or False.
- If argument type is got based on the default value, preserve the current behavior.
- 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.
Boolean argument conversion currently works like this:
Noneare accepted. Other values cause an error.Noneare passed as-is without conversion.True,1,yes, etc. depending on language configuration) yield BooleanTrue.False,0,no, ...) yield BooleanFalse.None, case-insensitively, yields actualNoneobject.This is rather surprising, because most values are not actually converted to Boolean values but instead passed as-is. Due to integers, floats,
Noneand strings having sane "truth values", this doesn't matter much if arguments are used likeif arg:. It is, however, strange that if you have a keyword likedef example(arg: bool), the values that are passed in are not guaranteed to havebooltype.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):
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 orNonein addition to Boolean values, you can usearg: bool | strorarg: bool | None. If you only want to accept certain strings, using something likearg: bool | Literal["LEADING", "TRAILING"]is very convenient and that's what the standard libraries nowadays use.My concrete proposal is this:
bool, convert values all accepted values either toTrueorFalse.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.