~bool deprecation

I would say, beyond that, bool is specifically the ints 0 and 1 too. I’ve used that many times to hack string formatters by multiplying the string by a bool. Since it’s totally valid to multiply a string by 0 to mask it out.

Another reasonable example of something I’ve done before that relies on bool being 1/0:

models: list[Model]

errors = len(models) - sum(m.valid() for m in models)

Which would fail if bool wasn’t 1 and 0

4 Likes

This is all valid interpretation and all of this will continue to work. The only thing that is removed is the ~ operation, specifically because it leads outside the 0, 1 space and that was was confusing to users.

1 Like

Except lots of things “lead outside the 0, 1 space”. For example, this isn’t uncommon at all:

num_true = sum(map(predicate, iterable))

As in, e.g.,

>>> sum(map(lambda i: not i % 3, range(12)))
4

which counts 0, 3, 6, and 9. Given that bool is a subclass of int, “the surprise” is when bools don’t act like ints.

IMO it wasn’t “broken” to begin with. What next? Will we also “deprecate”, e.g.,

>>> True / True
1.0

? I understand why that too could be surprising to someone coming from, say, C++ – but they’re not using C++ then, and should learn the language they are using.

6 Likes

I only brought this up because I saw some discussion of True being mapped to -1 on the GitHub discussion and wanted to make sure that these common situations were documented somewhere in all this.

And yes, this was mentioned before, but I don’t think “you shouldn’t do that” is really a good counter since this is something that anyone who spends some time using the language will probably end up abusing at some point, and it’s surprisingly difficult to find/debug if it was suddenly changed.

1 Like

I haven’t seen true be -1 since my days of BASIC programming… blast from the past.

3 Likes

The difference being that such cases are logically unambiguous and they solve a real problem.

In contrast the correct interpretation of ~bool depends on the fact that it’s an unsigned integer. It’s a fact that people are prone to getting this wrong and think ~True → False, ~False → True. This leads to real bugs because if ~condition is always true. Looking only at the first page of Code search results · GitHub approximately half of the entires exhibit this bug.

OTOH, I have yet to see a real-world use case for the actual mapping ~False → -1; ~True → -2. If such a case exists ~int(b) can be used and IMHO would be the clearer communication of intent anyway.

That is the background. We can now decide. Do we opt for purity (bool is implemented as int and therefore you can invert its bit pattern) and tell people to RTFM. The world is a dangerous place and you must know what you’re doing. Or do we want to protect users from accidental misuse and disallow this action, which is formally possible, but not needed in practice.

Nothing next. This is a unique case: There are no other operations that are logically meaningful on int and bool, but yield different results. True / True and sum have no meaning in a pure bool world. The only reasonable interpretation is as numbers 0,1. In contrast ~ has logical ambiguity and the result depends on the underlying integer representation. Is it unsigned or signed, in case of unsigned how many bits?

1 Like

The real-world fear is that if foo.f is a function that with the documented signature f(c: int) -> float, if I use the otherwise reasonable pattern

from foo import f

foo_len = sum(f(x > 0) for x in xs)

now this might crash if foo.f uses ~c in it’s implementation. But as a user I’m not supposed to care about the implementation details of f.

So the big issue is not that ~bool solves a real-world problem, but that prohibiting ~bool beyond the static type-checker level introduces problems.[1]


In practice this hasn’t bitten me yet because ~int is an uncommon operation.

I also thought the ship has sailed at this point. I’ve given my opinion, but there’s not much point in fighting about it anymore, because this is now a (planned?) Python feature.


  1. Besides which, ~bool deprecation - #80 by tim.one did just demonstrate a specific usecase for ~bool. ↩︎

1 Like

It doesn’t HAVE to have sailed; deprecations CAN be reversed. It all depends on who feels strongly enough among the core devs.

4 Likes

Then do we make it easier to understand the issue or do we remove the ability to create the issue? I am currently in favour of more instruction/explanation when mixing operators and types and keeping the ability to use bitwise operators on bools and explaining that bools are also ints which can lead to …

1 Like

I acknowledge this topic. It’s a potential issue. How relevant that is in practice is to be seen. This is also why there is a long deprecation period set to 4 releases. We’re now to years in and I haven’t seen any complaints.

Deprecations can always be reverted or extended, if there are good reasons. I’m not clear though what the process is and who decides that.

In the end, this is a trade-off: Either we leave an API that is prone to misuse and that leads to hidden errors (if ~condition will just silently not do what you want). Or we remove it, risking creating clear errors in very rare valid use cases, that can easily be worked around by an explicit int() cast.

Improving documentation is always nice, but I doubt that will significantly reduce misuse. Only few users will read the documentation to that level of detail.

1 Like

Possibly, but removing the capability might make the operators less easy to learn. If we have the True/False to 1/0 automatic conversion when bools are used in an int context, then the implications need to be understood, and maybe this particular case should be highlighted in a more general discussion. I remember that “Bools are not ints, they have representations and conversions in each type that are useful though”.

2 Likes

That’s not true. You’ve seen mine (and I think also some others).

3 Likes

Sorry, you are right. I had forgotten this. There may be a low single digit number. I suggest we collect these cases in a single place, so that they don’t get lost. I’ve created Real world impediments through ~bool deprecation for this.

I cry when I read this debate.

The inconsistency of disallowing ~x when x is a bool while allowing it when x is an int trumps the lack of a use case here.

Arithmetic is hard. Let’s not make it harder to accommodate people coming from some other languages.

9 Likes

Do I understand your comment correctly that you are opposed to the bool deprecation? You were originally the one encouraging turning the proposal into a PR in bool(~True) == True · Issue #82012 · python/cpython · GitHub.

To be clear: Reconsideration is ok. Maybe we are now smarter than back then. If you (and maybe the PR approvers) as proponents of the change think it’s not a good idea anymore, I won’t insist.

1 Like

Right, I’ve changed my mind. Or maybe I wasn’t thinking far enough ahead at the time.

I would be okay if ~b where b is statically typed as bool might trigger a warning in linters or static type checkers.

13 Likes

Defined by the language; implementations have no choices here. However they implement it, they must create the illusion that ints are represented in 2’s-complement with an “infinite number” of sign bits.

>>> ~True == -2
...  DeprecationWarning elided ...
True
>>> ~False == -1
...  DeprecationWarning elided ...
True

Those must be true under all Python implementations, as also are things like -2 & 7 == 6. Every bit is defined. The actual representation isn’t exposed. For example, CPython happens to store ints as an absolute value plus a distinct “sign” bit, but that’s invisible at the language level.

People mistakenly using “~” for logical “not” have deeper conceptual gaps than a deprecation warning can fill :wink:

1 Like

That’s a tad facetious, as “~ for logical not” has been popularized enormously for just that purpose by numpy, pandas, etc. The distinction being that it only works for arrays, but it’s hardly a stretch for users to want to try this as general boolean negation.

3 Likes

An array full of bools has a lot of similarity to an integer full of bits, though. When you negate the array, you’re flipping each individual bit.