Python already has “boolif”:
if foo is True:
print('true')
Python already has “boolif”:
if foo is True:
print('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.
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
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).
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__
?
>>> 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'
A 10% difference in speed in my py39 (I did check!)
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.)
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.