The troubles with Boolean inversion in Python
The Python bitwise-inversion (or complement) operator, "~", behaves pretty much as expected when it is applied to integers—it toggles every bit, from one to zero and vice versa. It might be expected that applying the operator to a non-integer, a bool for example, would raise a TypeError, but, because the bool type is really an int in disguise, the complement operator is allowed, at least for now. For nearly 15 years (and perhaps longer), there have been discussions about the oddity of that behavior and whether it should be changed. Eventually, that resulted in the "feature" being deprecated, producing a warning, with removal slated for Python 3.16 (due October 2027). That has led to some reconsideration and the deprecation may itself be deprecated.
The problem was reported in 2011 by Matt Joiner who was surprised by the outcome of some tests that he ran:
>>> bool(~True)
True
>>> bool(~False)
True
>>> bool(~~False)
False
>>> ~True, ~~True, ~False, ~~False
(-2, 1, -1, 0)
That last example demonstrates how those unexpected results came about:
True is effectively just an alias for one and False is
zero. When those values are inverted, they do not really
act in a Boolean kind of way. In Python, any non-zero value is treated as
true in a Boolean sense, and the complement of one is -2, both of which
evaluate to
true. Python defines
its integers as using two's
complement representation.
History
The bool type, True, and False were not added to the language until Python 2.3 in 2002, though the feature was infamously backported to the 2.2.1 bug-fix release prior to 2.3. PEP 285 ("Adding a bool type") described the feature in some detail; it is clear that using an integer value was done purposefully, for backward compatibility, at least in part. The PEP abstract explains:
The bool type would be a straightforward subtype (in C) of the int type, and the values False and True would behave like 0 and 1 in most respects (for example, False==0 and True==1 would be true) [...]
The author of the PEP, Guido van Rossum, was the Python benevolent dictator for life (BDFL) at the time; the Review section of the PEP kind of foreshadows the problems that led him to step down from that role 16 years later:
I've collected enough feedback to last me a lifetime, so I declare the review period officially OVER. I had Chinese food today; my fortune cookie said "Strong and bitter words indicate a weak cause." It reminded me of some of the posts against this PEP... :-)
The PEP was silent about applying the complement operator to bool values, but the implementation allowed it. Joiner filed the bug in 2011 because he went looking for a C-like unary not operator ("!"), which is not present in the language, and ran into "~" instead. As Amaury Forgeot d'Arc pointed out, the logical not operator is what Joiner was seeking. The bug was closed the day after it was opened, because the behavior was deliberate.
But the problematic behavior popped up again in a 2019 bug report from Tomer Vromen, who noted that the bitwise and ("&") and or ("|") operators acted as expected (i.e. like the logical equivalents), while complement does not. In fact, the bitwise versions of the and/or operators returned a bool result, while "~True" returns an int -2 (and not True as the integer could be interpreted, or even False as the caller might expect). The bug report linked to a fairly lengthy python-ideas thread from 2016 that also discussed the problem. Both the bug and the thread noted that NumPy has a Boolean type that behaves as expected (at least by some) and returns False for "~numpy.bool_(True)".
In the thread, Van Rossum seemed to lean toward changing the behavior, but wanted to do it with a quick change for Python 3.6, skipping a deprecation cycle, or not at all. Python behavior seems fairly inconsistent, as he described:
To be more precise, there are some "arithmetic" operations (+, -, *, /, **) and they all treat bools as ints and always return ints; there are also some "bitwise" operations (&, |, ^, ~) and they should all treat bools as bools and return a bool. Currently the only exception to this idea is that ~ returns an int, so the proposal is to fix that.
More recently
The idea seems to have just died out in 2016, and again in 2019, but was resurrected by Tim Hoffmann in a 2022 comment on the 2019 bug report. He proposed that ~ be deprecated for the bool type, which Van Rossum endorsed, suggesting that the deprecation be added for the then-upcoming 3.12 release. Earlier, Van Rossum clearly did not want to change the type of the result of ~bool to be a bool:
Because bool is embedded in int, it's okay to return a bool value that compares equal to the int from the corresponding int operation. Code that accepts ints and is passed bools will continue to work. But if we were to make ~b return not b, that makes bool not embedded in int (for the sake of numeric operations).Take for example
def f(a: int) -> int: return ~aI don't think it's a good idea to make f(0) != f(False).
In 2022, though, he was in favor of deprecating the use of the complement operator on bool values, rather than switching to a bool return type for complement. In the discussions about the behavior over the years, the main downside to it is that it can be confusing to users and that there is seemingly no real use case for it. For those who do end up getting confused, it is clearly not the right tool for the job, but the fact that NumPy and other libraries have normalized using bitwise complement to mean not muddies the waters.
The deprecation warning was duly added to Python 3.12 in 2023 from a pull request from Hoffmann. It gives a lengthy explanation when the exception is raised:
DeprecationWarning: Bitwise inversion '~' on bool is deprecated and will be removed in Python 3.16. This returns the bitwise inversion of the underlying int object and is usually not what you expect from negating a bool. Use the 'not' operator for boolean negation or ~int(x) if you really want the bitwise inversion of the underlying int.
One of the problems with deprecations is the visibility of the warnings; at various points, the DeprecationWarning exception was hidden by default because it too often was only seen by end users who were unable to fix the underlying problem. That changed back in 2017 to increase the visibility of the warnings, in part so that users could request fixes from library developers—deprecation in Python pops up fairly frequently in discussions about development of the language.
In August 2024, though, Barry Warsaw saw a GitHub email notification about the deprecation, which surprised him because he could not remember a wider discussion about it. He posted to the core development category to have that discussion, but he also wanted to talk about changes like this that can sometimes fly under the radar, so he started a parallel discussion as well. The question of "change visibility" seemed to reach a consensus that there was a problem in need of addressing, but there was less clarity on what might be done. Too much bureaucracy, in the form of PEPs or a more formalized change-management process, may negatively impact contributions, which largely come from volunteers; too little can lead to surprises like the deprecation of ~bool.
On the question of whether it should be deprecated at all, no real consensus was found, which has been the case throughout its history; some were strongly pro-deprecation because it is confusing and generally a footgun, while others lamented the inconsistency of only disallowing bitwise complement for the bool type and allowing all of the other arithmetic and bitwise operators.
Oscar Benjamin noted that
"use of ~ for logical negation is widespread
" in NumPy and
SymPy. Antoine Pitrou pointed
out that is because ~ can be overridden, unlike the logical
not. Benjamin agreed,
saying that PEP 335
("Overloadable Boolean Operators") would have allowed NumPy and SymPy to
take a different path, but it was eventually rejected
in 2012. Both Benjamin and Pitrou did not think ~bool was
particularly useful and were in favor of deprecation.
On the flipside, Bjorn Martinsson provided some examples of how he uses ~ on Boolean values. They are probably kind of obscure, but he has even publicized a use of the technique. A few others popped up in the thread with use cases as well.
Hoffmann summarized the arguments that led him to propose the deprecation and author the code change to effect it. Since he believed it made sense to rid the language of this footgun, only two paths presented themselves: changing the behavior to a logical negation or deprecating and eventually removing ~bool. He saw no good migration path for switching to negation, though, so he opted for deprecation. The discussion continued on for another month or so before winding down without any firm conclusion. There was talk of a PEP, but that did not come about either.
The thread sparked up again in October 2025 and Hoffmann responded to a query about the PEP, pointing to the bug discussion and his summary earlier in the thread. At that time, Tim Peters also posted about a change that he had to make to his code because of the deprecation; he thought it was far too late in the history of the language to be making breaking changes of that sort:
All computer languages have quirks. Python is, IMO, too mature and widely used now to risk changing much of any visible behaviors, short of screaming bugs, or (but less compellingly so) accidents of implementation that were never documented as "advertised" behavior.There's nothing surprising about ~bool to people who learn the language. bool is a subclass of int in Python, period. I don't give a hoot how it works in other languages. The time for that kind of argument was when Python's semantics were first crafted. It's too late for that now.
The present
Things went quiet again until mid-February 2026, when Hayden Welch posted a concern, but had misinterpreted what was being deprecated. It led to more discussion, naturally, much of it between Hoffmann and Peters, along with a reminder from Stefan Pochmann about his use case. That caused Hoffmann to start a parallel thread to gather real-world impacts of the deprecation, which currently just has a link to Pochmann's use case and a brief mention of the deprecation (or, really, someday elimination) of ~bool being a violation of the Liskov substitution principle (which had also come up elsewhere in the discussions). Essentially, if bool is to be a subtype of int, it has to be able to be used wherever an int can be and ~ surely qualifies.
In the main thread, though, Van Rossum said that
the discussion made him cry. "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.
" That was, of course, a complete reversal of his position back
in 2023, and also different from his 2016 advocacy of a quick switch to a
Boolean result for ~bool. In another message, he confirmed
the reversal:
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.
Around the time of Van Rossum's change of heart, the thread seems to have picked back
up, at least for a bit. In response to Peters's argument
that people mistakenly using ~ for logical not are
terribly confused, "H. Vetinari" claimed
that they were not, since
NumPy and the like have popularized the idea, but "that it only
works for arrays
". Peters was strongly
convinced that the NumPy model would not be good for Python as whole to
follow, however. For one thing, it works on more than just arrays, "but the
conceptual model is baffling
". He provided a number of examples
showing how NumPy is internally inconsistent in its handling of its
bool type.
Everything Python does follows from that bool is a subclass of int. That's all you have to remember. numpy's bool stands as unique in its type system, and is not even "a numeric type" there - although various operations' special cases make it act like one in various ad hoc ways.It's simply incoherent, a grab-bag of special cases. The core language shouldn't budge the width of an electron to try to cater to any such stuff.
Matthew Barnett raised
the seeming oddity of bitwise & and | returning a
bool result, while ~ does not; that was inconsistent in
his eyes, as it was in plenty of others' along the way. James Dow largely
or completely demolished
that argument with extensive references to the language documentation.
The language reference pretty clearly shows that the existing behavior is
required; an implementation is not actually Python without allowing
~bool. Since bool is an int, the
bitwise and/or operators are consistent as well: "True | False
must return an integer with a value of 1 (which True is) and
True & False must return an integer with a value of 0 (which
False is).
" Tom Fryers also had a lengthy
explanation that showed why the Liskov substitution principle matters,
and that real breakage results from deprecating the
~bool operation, even though that operation is perhaps weird and unlikely.
Hoffmann seems amenable to reversing course on the deprecation. In the abstract, that should be easy enough to do; code that changed due to the warning will continue to function just fine if the warning goes away. It is not entirely clear how a decision like that would be made, but one guesses the steering council will be brought in at some point to make a pronouncement. There is no huge rush, at least until the time comes to turn the warning into an exception, which is a year or more off at this point.
Overall, the mood seems to be shifting away from deprecation. Using inversion on a bool is a bit of a dark corner of the language, for sure, and it may have been a mistake not to create a separate Boolean type, certainly some in the discussions believe so. The confusion comes to those who think the language does have a separate Boolean type, and it would be nice to find a way to warn them, but removing the feature altogether seems like a step too far.
The long journey for ~bool is probably not over, but perhaps some kind of ending will come before long. This episode demonstrates a number of aspects of the Python development process over the years, from its more freewheeling days 20 or more years ago through its more stodgy aspect these days. Throughout, we see the general cordiality and collegial nature of its discussions; one suspects we have not seen the last of this odd corner of the language, but that further discussion or development will proceed along the same genial lines. Both the language and the community are rather mature at this point—and it shows.
| Index entries for this article | |
|---|---|
| Python | Boolean |
| Python | Deprecation |
