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

Python already has “boolif”:

if foo is True:

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):

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:

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):

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
if strict_lt(expr, 1.0):  # TypeError if not a float

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

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

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

from functools import partial

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

    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)

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

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.