boolif - A built-in conditional if statement, that requires the boolean type

Feature or enhancement

An alternative built-in keyword to if: boolif, that must act on a bool type or else fail. All “truthy” options such as [], None, 0, 1 would fail with this variant.

Pitch

Frequently there have been bugs in code I have used and created, where a conditional statement is expected to receive a boolean, but due to mistake, it does not. A simple case I encountered earlier with Path:

from pathlib import Path
path = Path('test')
if path.exists:
   print('true')

The mistake here is that exists is a function that should be called like so:

from pathlib import Path
path = Path('test')
if path.exists():
   print('true')

Because if accepts truthy args, the first version does not raise an error, and the bug is not detected. You could of course add an assertion that resolves this as below:

from pathlib import Path
path = Path('test')
path_does_exist = path.exists()
assert isinstance(path_does_exist, bool)
if path_does_exist:
   print('true')

But this is quite long, and depending on the scenario may have a runtime cost. The syntax I propose is as follows:

from pathlib import Path
path = Path('test')
boolif path.exists():
   print('true')

This is now brief, and by making it a built-in feature, I believe there could be way for there to be no speed loss from it. This would not replace if, and simply be an additional option for those that require it.

If you care about such errors, you should probably use a static type checker. If you are using a static type checker, you can easily do that with

def test(b: bool) -> bool:
    return b

if test(path.exists): # fails type checking
    ...
5 Likes

Many linters catch error for you, although I agree that truthiness of a variety of objects is a source of bugs.

2 Likes

As i mentioned to Mark on GitHub, mypy catches this bug with the default configuration settings, and there’s no need even to define the separate test() function suggested by @jeanas:

https://mypy-play.net/?mypy=latest&python=3.11&gist=9eb36ff2bb5d3609ed74ae175f2a921f

from pathlib import Path
path = Path('test')
if path.exists:  # mypy error: Function "Callable[[], bool]" could always be true in boolean context  [truthy-function]
   print('true')

It’s true that this is a common bug to encounter, but I don’t see the need for new syntax here.

3 Likes

Having programmed in C#, I am massively against this feature. The fact that everything has a truth value is a hugely helpful feature, and if you’re trying to fight against that, it’s much better to make your own enforcement function than to create a boolif:

def cond(x):
    if x is True or x is False: return x
    raise TypeError

if cond([]): # boom
    ...

Besides, if you had this, it wouldn’t be long before you’d find it extremely frustrating that you can write boolif but not boolwhile, and then it’d be irksome that the and and or operators don’t have boolean-enforcement versions, etcetera, etcetera, etcetera. Instead, do it yourself, with your own function, and then you don’t need to pollute the language with duplicate ways of doing everything.

6 Likes

Thanks all - just a note that I do use type checking, it’s more that this is something that frequently slips through the type checking net, as it is actually a valid entry, but not what the user intended. Your solution with the test func is pretty good - it does the job, but it has some drawbacks. You would either have to define this in every file, or have every file import this function, which seems pretty odd. Mypy catches the func case I describe above, but there are many other use cases. Take the below:

def test_func(i :int):
    if i == 1:
        return True
    elif i == 2:
        return False
    elif i ==3:
        return True

if test_func(4):
    print('here')

It’s possible this case could be resolved with a return annotation, but there are other examples.

Yeah I’m not sure I like the fact everything has a truth value - personally I would probably suggest replacing if altogether with boolif, although this has some obvious drawbacks for backwards compatibility. It’s a good point about and and or - perhaps everything is flawed in this way…

The below would be another variant of the “cond” function:

def cond(x):
    if isinstance(x,bool): return x
    raise TypeError

if cond([]):
    ...

This feels closer to a lint rule that needs type inference. If the rule didn’t require type inference I’d recommend writing a pylint/flake8 plugin to enforce this. But here you need to be able to identify type passed to the if expression. I know mypy supports plugins although unsure how extensible/easy plugin system is. I’d recommend exploring a mypy plugin that enforces this rule specific to your codebase.

1 Like

Are you sure Python is the right language for you?

In all seriousness: That is most definitely NOT going to happen. It’s a massive break of backward compatibility and would result in a serious loss of functionality. For most of us, the boolification of objects is a feature, not a bug.

Remember that, once you have your cond function (or whatever you want to call it), you can use it in any context - if, while, either side of and/or, anything at all. We do not need to turn Python into C#, nor do we need to turn Python into JavaScript.

5 Likes

Care to share some other examples, because as you say that was a poorly chosen one and does not help your case.

1 Like

Python’s a great language, but that doesn’t mean that questioning a design decision means that “it’s not the right language for you”.

I agree with Mark. I think everything having a Boolean value was a design error.

1 Like

As you should be able to tell from the “in all seriousness” immediately afterwards, that comment wasn’t entirely serious… do I need a sarcasm sign?

But it also wasn’t entirely meaningless. When you dislike a language’s major design decisions, it may be a sign that a different language is a better choice. Remember, this isn’t a small choice like whether the operator is spelled != or <> or whether 3+4j is a pair of integers or a pair of floats; this is something that permeates the entire language. You can’t ask Python to switch to pass-by-value, or to ask for def to become a non-executable declaration.

We have many languages available precisely because different language choices can all be viable. Some ideas don’t belong in Python.

2 Likes

More than sure.

The problem is that if and while makes an implicit call to bool() AFAIK. This seems a little unpythonic: explicit is better than implicit.

On the other side, it’s quite more elegant and readable to write, for example, if some_sequence instead of if bool(some_sequence)

I’m not against the introduction of a strict if, but I don’t think it will ever implemented. IMHO your cond function is a useful compromise.

Its more than “possible”, it is resolved with a return annotation:

[steve ~]$ mypy return_None_check.py 
return_None_check.py:1: error: Missing return statement
Found 1 error in 1 file (checked 1 source file)

Regardless of arguments about the pros and cons of truthiness testing compared to strict Pascal-like bool testing, the decision to duck-type truthy values is baked so deep into the language that it isn’t going to change and there is no point debating it.

Adding boolif will just open you to even more errors, namely writing if when you intended to write boolif.

The practical solution here is to use a static checker: a type checker or a linter or both. They will pick up these sorts of errors for you, and many more.

Since you say that you are already using one, and we know that they are capable of detecting these errors, I’m not sure what else there is to say in this thread:

  • Python is not going to drop duck-typing from its design.
  • You already have a solution to your problem.
  • So this boolif seems to be redundant and unnecessary.

shrug

6 Likes

I agree this is by design from Python 1 probably, and it’s impossible to change, even if someone prove that the strict behavior is better (quite debatable…)

But I don’t think it useless to discuss it. Mark pointed out a problem. Honestly I had some similar problem with the implicit if bool(x) (not sure why you say this is duck typing). I think that, even if the proposed solution is not going anywhere, is constructive to know that there’s a problem with the implicit bool() and how you can solve it.

1 Like

That sort of discussion belongs in the general help category, though. This category is specifically for ideas which do have a chance of being implemented.

Thanks for all the responses on this! Really appreciate the feedback on it from both sides of the debate. I’ll have a think about further cases - there are plenty I’ve encountered the type checker couldn’t detect, but I’m wondering how many of these come from using libraries that aren’t fully rigorous about types.

I think there is something here that could be implemented - perhaps boolif isn’t the answer, but it’s definitely an issue that people keep on facing. I think implementing something like cond is fine in some way, but an additional import statement in every single program you ever write feels like a sizeable con. Plus somebody then has to find out what this function does if it is in a library, and they might feel a little bit put out when they find it’s 3 lines of code.

Perhaps the answer lies in some extension to typing, that allow you define type hints in line. See below:

from pathlib import Path
path = Path('test')
if bool@path.exists():
   print('true')

I know this isn’t valid syntax currently, but something along these lines I think could be very powerful. Kind of like walrus but for types. Can make a new discussion thread if people think it’s a separate discussion

+1 to this.

Even if you’re not using a static type checker because your code is not
yet sufficiently annotated (example: me!) you can write a runtime
function like Jean’s example:

 def isbool(b):
     if not isinstance(b, bool):
         raise TypeError
     return b

and use:

 if isbool(idiomatic python "truthy" thing here):

Avoids breaking code, avoids adding a clunky new keyword, and is just as
in-your-face i.e. overt as the suggested “ifbool” keyword.

I’m a big fan of the truthy idiom in many cases because it makes code
far more readable. So I’m for a shim type checking function (at static
type analysis time or runtime depending on dev environment) instead of a
special variety of “if”.

Cheers,
Cameron Simpson cs@cskk.id.au

2 Likes

Other objections aside I’m not sure how this actually would save users who are not using strict type checking from this problem. If I don’t know if a callable returns a strict bool or just an object whose truthiness is meaningful than I am likely to to start defensively calling the function bool(), e.g.

from pathlib import Path
path = Path('test')
boolif bool(path.exists):
   print('true')

And now we are just back where we started. The problem OP has here is that there is no meaningful interpretation of the truthiness to a callable (as best as I know there is no “empty callable”) yet a truthiness is given anyway.

The only way this can be solved in a Python workflow right now is with strict type checking, which actually solves a wider set of problems such as receiving an unexpected type that does have a meaningful interpretation of the truthiness but misinterpreting it because it was not the type you expected.

1 Like

Also worth noting: What returns True/False today might tomorrow return something different, so long as the truthinesses are maintained. Normally that wouldn’t be considered a majorly significant change.

1 Like