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.
Yeah I think this is a good point as it would circumvent the point of boolif. In some way I consider the type annotation free world of python to be something of the “Wild West” in terms of validating types. My concern is much more that currently (with the exception of mypy as demonstrated earlier, in this function case) - type checkers really don’t offer much help with this problem, because there’s no way to flag cases that are technically valid but not what was intended by the user (without an additional line).
This is what the “in line type checking” I described above intends to solve, although I think this probably does deviate too much from the original boolif idea. I will think on more examples of where this would be useful, and open a new discussion thread, as I think the boolif idea itself has many more issues.
I don’t understand your example here, but note that you can get mypy to complain even more about this kind of thing by running mypy --enable-error-code truthy-bool
on your code. This lint has a high number of false positives, so it is disabled by default even in mypy’s --strict
mode, but it sounds like you might find it helpful.
If mypy has this feature but other type checkers don’t, that just sounds like an argument for filing feature requests with the other type checkers (or, if this matters a lot to you, switching from your current type checker to mypy).
Almost every object supports the “bool” protocol, and truth checking like if obj: ...
checks whether obj
quacks like a true value or a false value.
-
Empty lists, dicts and tuples quack like False. Empty strings, None and numeric zeroes quack like False.
-
Non-empty lists, dicts and tuples quack like True. Non-empty strings, arbitrary objects and numeric non-zeroes quack like True.
That makes it duck typing in my book.
Oh please no. Excessive use of type hints are already bloating Python code, but at least they are (mostly) limited to annotations in function signatures and a few other places. Adding annotations to arbitrary parts of expressions would be a horrorshow.
I’ve made a separate post for the in-line type hints suggestion, if people want to check it out. I’m quite convinced by the issues people have raised with boolif, so I’m not sure it’s worth pursuing further, but I think this new solution covers many more cases, the boolif one included:
I’ve often thought that a “strict” if would have been a better choice, Being explicit can be helpful, and there’s nothing inherently “false” about a zero value. But that ship as LONG since sailed
But this example (function objects being Truthy) does bring up a point – is it really helpful that ALL object have a Truthyness? numbers,containers, etc make a lot of sense, but is is evey useful for a function to be truthy?
Anyway, antoher way to make your code a little less prone to these kinds of errors is to explicitly look for True:
if somethign is True:
...
That would have caught the example bug.
But as stated by others, that could break a lot of code that IS passing things around expecting “truthyness”, rather than an actual Bool value.
Also – this falls into what I’ve always thought most type errors are: shallow bugs – if this kind of error is not caught by tests, you don’t have very good test coverage – and when it does fail, it’s usually pretty easy to see what’s wrong and fix it.
Yes, because it allows you to have a variable that might contain a function, or might contain None, and you can use its truthiness to check for that. Extremely convenient and quite common.
In that case, you should be testing using is None
—not implicit boolean conversion.
Why? Truthiness is entirely guaranteed. What if I have a number that could be 0 and could be something else - am I supposed to test with == 0
rather than boolean conversion?
This is a feature, not a bug.
Why? Truthiness is entirely guaranteed.
By writing things out explicitly, your code
- defends against introduction of non-function objects, including objects with
__bool__
methods that have unintended consequences, and - reminds the reader that you’re filtering out
None
elements rather than burdening the reader with figuring out which implicitly false objects are being filtered.
Yes, for the same reasons as above.
Or the reader just understands that, if there’s a function, you call it. If there isn’t, you don’t.
if func: func(thing)
That code doesn’t need to be rewritten like this:
if func is not None: func(thing)
if callable(func): func(thing)
if (func is not None) is True: func(thing)
These are more explicit, but are they any more correct?
The standard interpretation of “Explicit is better than implicit” is that “Explicit” means “Code that I like” and “Implicit” means “Code that I don’t like”. Everyone’s idea of good code is different, and there is always a way to be “less implicit” in a way that makes your code worse.
At this point, the thread should probably be moved out of Ideas. I don’t think there’s anything here that is going to be an actual language idea.
None of those examples are like what I suggested since calling the object nearly always succeeds whenever the object is callable, and nearly always fails whenever it isn’t. Whereas, there are plenty of objects that are falsy that are not none, or that don’t compare equal to zero.
But how is that usefully different? If there’s a callable that happens to be falsy, it’ll be ignored, but that would be a VERY weird situation. OTOH, if something other than a function slips through where it’s expecting a function, I would expect a TypeError, not silently ignoring it. So the truthiness test should be fine; the is None
check is also fine, but unnecessary; and checking if it’s callable is a declaration in code that it could be any object, but we want to call it only if it’s callable, which is very different in concept from “if there’s a function, call it”.
Yeah, but you’ll get the type error much later. If those callables are stored in a some callback list, they may not be called for ages.
And that was only my first point about defensive coding. It also makes things easier for readers to compare to 0
or 0.0
or checking identity with None
.
Irrelevant to the example I was talking about; are you talking about something completely different? Am confused.
Also confused. What do you mean? Are you saying that we should have explicit type conversions such that 0 is not equal to 0.0?
Your argument is that after filtering the list of callables, you’ll call them and then you’ll get type errors if anything else sneaks in. But you may not call them immediately, so this isn’t always a good argument.
Are you saying that we should have explicit type conversions such that 0 is not equal to 0.0?
No. I’m saying that comparing with 0 or 0.0 or None:
When I was young and foolish and learning Pascal, I (like many beginners) got into the bad habit of writing bool tests like this:
var:
myflag: boolean;
if myflag = true then ...
(Please excuse any errors in that snippet, my Pascal is very rusty.)
But of course, since I didn’t trust the bool variable to be a bool, to be extra sure I should have written
if (myflag = true) = true then ...
or even
if ((myflag = true) = true) = true then ...
Its hard to know when to stop.
I won’t say that your advice to compare directly against True always falls into the same category. After all, maybe somebody needs five-valued boolean logic:
if flag:
if flag is True:
do_this()
else:
do_that()
else:
if flag is False:
do_spam()
elif flag is None:
do_eggs()
else:
do_cheese()
But it goes against PEP 8, and many people consider it redundant at best and an antipattern at worst.
Even if that extra comparison is justified, many code reviewers will see it as unnecessary unless there is a comment explaining why it is needed.
And of course comments lead to comments-as-lies, as comments are infamous for becoming out of date and inaccurate. Coding is hard
I completely agree with your statement here:
There are famous examples of powerful type systems finding astonishingly deep bugs, but generally type errors are shallow, and the best static type checker in the world doesn’t remove the need for a good test suite.
I think the right solution to this issue is to use one or more of
- a linter;
- a static type checker;
- unit tests
rather than new syntax, or redundant bool comparisons that will earn you snide comments about “not understanding boolean logic” :-/
No idea where that notion of “filtering the list of callables” came from. I was talking about a pretty common pattern where you either have a function or don’t have a function, and the obvious thing to write is:
if func: func(thing)
So… it’s being called. Right here. If you give it something that is truthy but not a function, you get a TypeError. It’s pretty obvious what’s happening.
Where did func
come from? Could be anywhere, but that’s a completely separate problem. (Quite likely it was a parameter to a function at some point. Who knows, who cares.) But right here, is it really necessary to test whether it is not None
, or can we just check whether it exists? Either you have a function, or you don’t. That’s all you have to worry about.
Actually, that wasn’t advice in general, only a perhaps better alternative than writing a full fledged check-if-this-is-a-boolean-first function.
I agree on all the rest of your points.