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

I got to say, I don’think I’ve EVER doen that – it is a very common pattern for a variable to be “unset” by being set to None, and if you need to check for that you use:

if var is None:
    do_something_with_var

In fact, I’ve seen bugs where folks use:

if var:
    do_something_with_var

and end up with errors when var happens to be falsy – there is a big difference between “anything Falsy” and “None”.

I don’t see any reason to treat things that are supposed to be None or a callable differently, even though it is very unliley that a callable will be Falsy. Not impossible mind you – a callable class could very will use bool for some other useful purpose – like indicating emptyness.

And it’s similar for:

if var == 0.0:

There is nothing inherently “Falsy” about zero – it all depends on what that value means – if I want to know if it’s unset, I check for None, if I want to know if its value is zero, I should check for that, just like I would check for its value being 5, or greater or less than some value.

I do like, and use, Truthiness to check for an empty container for the most part – as in my mind that is inherently Falsy.

This seems to me analogous to rarely using bare except: when you write a try:except block you should know what kind(s) of Exceptions you are prepared to handle, and you don’t want any other Exceptions to leak though. In an “if” block you should know what kinds of Truthiness you are expecting, and you don’t want other Truthy objects to leak through.

Now that I"ve written all that – I agree, this is not a discussion that belongs in ideas … Is there a “Pythonic style” forum?

2 Likes

Python already has “boolif”:

if foo is True:
    print('true')
5 Likes

The behaviour there is different (although probably sufficient for many use cases). if None is True: ... is a no-op, but boolif None: ... would be an error.

That’s a fair point - but this is precisely what static analysis is for.

1 Like

This discussion got me wondering, how exactly does static type checkers deal with the is operator? I’d guess they need to be the same type but a quick glance through the mypy docs didn’t provide me with answers.

I have seen some linters warn that you are using is when == is required I have seen warning.

% py3
Python 3.11.2 (v3.11.2:878ead1ac1, Feb  7 2023, 10:02:41) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
:>>> x  = 93
:>>> if x is 93:
:...     print('it is 93!')
:...
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
it is 93!
:>>> y = 93
:>>> if x is y:
:...      print('it is 93!')
:...
it is 93!
:>>> x = 10374
:>>> y = 10374
:>>> if x is y:
:...      print('it is y!')
:...
:>>>

This may work if 93 has been interned, but cannot be relied on.
It fails with 10374 in cpython as its not iterned.

The arguments to is don’t have to be the same type. [] is {} doesn’t raise an exception, it just returns False.

To a type-checker, there’s nothing special about is. It is an operator which accepts two operands of any type and returns a bool True/False flag. If it were a function, it would have this signature:

def is_(a: Any, b: Any) -> bool :
    ...

It literally is a function, and it has exactly that signature: operator — Standard operators as functions — Python 3.11.2 documentation :wink:

The below is similar to the point stated in the in-line type hints discussion, but I’m copying here in case it’s useful to people:

boolif would be equivalent to this, without the need to add additional functions:

from typing import assert_type as at

if at(foo, bool):
  print('here')

The only issue here as I mention in the other post is brevity, although this syntax can be made pretty brief.

To address the is discussion, these two operations are treated differently by type checkers, below is a quick comparison:

if None is True:
    pass

The above does not raise a type issue, as it simply is always False

from typing import assert_type as at

if at(None, bool):
    pass

This I believe does however as it asserts that the None is of bool type, analogously to boolif

Ny main problem with the boolif as keyword is that it seems utterly superflous, especially as a keyword: it is basically a simple function:

def boolif(value):
    if not isinstance(value, bool):
        raise ValueError
    return value

There is no reason to add keywords to the language for such simple use cases.

Slightly broadening the scope, having strict comparison operators in a small module somewhere (as functions, not keywords) might occasionally save a couple of keystrokes.

if strict_eq(expr, True):  # TypeError if not a bool
    pass
if strict_lt(expr, 1.0):  # TypeError if not a float
    pass

This is the sort of thing that really should be in your own personal toolkit, since what’s good for you is not the same as what’s good for other projects. It is equally “obvious” that your strict_eq function ought to test whether type(expr1) is type(expr2), and that it should test whether isinstance(expr1, type(expr2)), and that it should be given an explicit type that it tests against. All three are entirely valid (and I wouldn’t use any of them in my projects).

1 Like

I think this is an interesting idea - I suppose perhaps this is more in the role of type checker than an assertion, so I would argue that assert_type may save the day again, for no runtime cost:

from typing import assert_type as at

if at(expr, bool): # type checker error if not a bool
    pass

if at(expr, float) < 1.0:  # type checker error if not a float
    pass

And you can make a similar idiom for runtime checks as well:

from functools import partial

class InstanceOf:
    @staticmethod
    def __getitem__(type_):
        return partial(InstanceOf.__call__, type_)

    @staticmethod
    def __call__(type_, obj):
        if not isinstance(obj, type_):
            raise TypeError(f'expected {type_.__name__}, '
                            f'got {type(obj).__name__}')
        return obj

instanceof = InstanceOf()

# ---

if instanceof[int, float](5) == 5.:  # ok (square brackets for easy tuples)
    pass

if instanceof[bool]([]):  # TypeError
    pass

Why bother constructing instances of your instanceof class when you can (ab)use __new__ and __class_getitem__? :wink:

>>> from functools import partial
>>> class instanceof:
...     @classmethod
...     def __class_getitem__(cls, params):
...         return partial(instanceof.__new__, params)
...     def __new__(cls, obj):
...         if not isinstance(obj, cls):
...             raise TypeError(
...                 f"Expected {cls.__name__!r}, got {type(obj).__name__!r}"
...             )
...         return obj
...
>>> if instanceof[int, float](5) == 5:
...     pass
...
>>> if instanceof[bool]([]):
...     pass
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __new__
TypeError: Expected 'bool', got 'list'
1 Like

A 10% difference in speed in my py39 (I did check!) :wink:

1 Like

And, for that matter, it seems even more obvious that it should return TypeError rather than ValueError (though I’m not aware of a counterargument to that one, unlike your examples).

I’d just like to point out that it’s hugely helpful in C# because conversion to bool is a bit awkward to spell. In Python, it’s spelled bool(), surrounding the expression.

Having to do something explicit to get a boolean value would also make it a lot easier for the inexperienced to understand the “truth value of an array is ambiguous” Numpy gotcha.

(I admit it’s not very convenient once and/or/not get involved.)

1 Like

It’s still completely unnecessary to have to spell it at all, since there’s nothing an if statement can do with a value other than decide to execute, or decide to not-execute.

The numpy gotcha comes from a conflict between most of Python, where “empty list is false, non-empty list is true” and numpy, where operations get vectorized. The same would happen with an explicit boolification step, so all it’d do is add unnecessary boilerplate while keeping the ambiguity.