Skip to content

gh-82012: Deprecate bitwise inversion (~) of bool #103487

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ are always available. They are listed here in alphabetical order.
or omitted, this returns ``False``; otherwise, it returns ``True``. The
:class:`bool` class is a subclass of :class:`int` (see :ref:`typesnumeric`).
It cannot be subclassed further. Its only instances are ``False`` and
``True`` (see :ref:`bltin-boolean-values`).
``True`` (see :ref:`typebool`).

.. index:: pair: Boolean; type

Expand Down
54 changes: 33 additions & 21 deletions Doc/library/stdtypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,39 @@ number, :class:`float`, or :class:`complex`::
hash_value = -2
return hash_value

.. _typebool:

Boolean Type - :class:`bool`
============================

Booleans represent truth values. The :class:`bool` type has exactly two
constant instances: ``True`` and ``False``.

.. index::
single: False
single: True
pair: Boolean; values

The built-in function :func:`bool` converts any value to a boolean, if the
value can be interpreted as a truth value (see section :ref:`truth` above).

For logical operations, use the :ref:`boolean operators <boolean>` ``and``,
``or`` and ``not``.
When applying the bitwise operators ``&``, ``|``, ``^`` to two booleans, they
return a bool equivalent to the logical operations "and", "or", "xor". However,
the logical operators ``and``, ``or`` and ``!=`` should be preferred
over ``&``, ``|`` and ``^``.

.. deprecated:: 3.12

The use of the bitwise inversion operator ``~`` is deprecated and will
raise an error in Python 3.14.

:class:`bool` is a subclass of :class:`int` (see :ref:`typesnumeric`). In
many numeric contexts, ``False`` and ``True`` behave like the integers 0 and 1, respectively.
However, relying on this is discouraged; explicitly convert using :func:`int`
instead.

.. _typeiter:

Iterator Types
Expand Down Expand Up @@ -5394,27 +5427,6 @@ information. There is exactly one ``NotImplemented`` object.
It is written as ``NotImplemented``.


.. _bltin-boolean-values:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello. The removal of this ref tag broke intersphinx downstream. I have not tracked down yet which middleware is calling this tag when API docstring has a "bool" mentioned. I suspect it is numpydoc. Is it not possible to reuse this tag for your new section above?

xref astropy/astropy#15428

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you open a PR, I can backport it to the 3.12 branch

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Please see #110371 . Thank you for your consideration!


Boolean Values
--------------

Boolean values are the two constant objects ``False`` and ``True``. They are
used to represent truth values (although other values can also be considered
false or true). In numeric contexts (for example when used as the argument to
an arithmetic operator), they behave like the integers 0 and 1, respectively.
The built-in function :func:`bool` can be used to convert any value to a
Boolean, if the value can be interpreted as a truth value (see section
:ref:`truth` above).

.. index::
single: False
single: True
pair: Boolean; values

They are written as ``False`` and ``True``, respectively.


.. _typesinternal:

Internal Objects
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,12 @@ Deprecated
replaced by :data:`calendar.Month.JANUARY` and :data:`calendar.Month.FEBRUARY`.
(Contributed by Prince Roshan in :gh:`103636`.)

* The bitwise inversion operator (``~``) on bool is deprecated. It will throw an
error in Python 3.14. Use ``not`` for logical negation of bools instead.
In the rare case that you really need the bitwise inversion of the underlying
``int``, convert to int explicitly with ``~int(x)``. (Contributed by Tim Hoffmann
in :gh:`103487`.)

Pending Removal in Python 3.13
------------------------------

Expand Down
18 changes: 16 additions & 2 deletions Lib/test/test_bool.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,22 @@ def test_math(self):
self.assertEqual(-True, -1)
self.assertEqual(abs(True), 1)
self.assertIsNot(abs(True), True)
self.assertEqual(~False, -1)
self.assertEqual(~True, -2)
with self.assertWarns(DeprecationWarning):
# We need to put the bool in a variable, because the constant
# ~False is evaluated at compile time due to constant folding;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should test this behavior separately (doing something like with assertWarns(DeprecationWarning): exec("~False")).

Copy link
Contributor Author

@timhoffm timhoffm Apr 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean instead of or in addition to the current test? We originally had eval("~True") but I figured it's clearer to have the warning context only around the operation and not around a comparably complex eval() or exec(). I'm happy to change if there's a benefit of these though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have both. The existing tests check that the deprecation warning is emitted correctly at runtime. The new tests would ensure that if the operation occurs at compile time, we still emit the DeprecationWarning.

# consequently the DeprecationWarning would be issued during
# module loading and not during test execution.
false = False
self.assertEqual(~false, -1)
with self.assertWarns(DeprecationWarning):
# also check that the warning is issued in case of constant
# folding at compile time
self.assertEqual(eval("~False"), -1)
with self.assertWarns(DeprecationWarning):
true = True
self.assertEqual(~true, -2)
with self.assertWarns(DeprecationWarning):
self.assertEqual(eval("~True"), -2)

self.assertEqual(False+2, 2)
self.assertEqual(True+2, 3)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The bitwise inversion operator (``~``) on bool is deprecated.
It returns the bitwise inversion of the underlying ``int`` representation such that
``bool(~True) == True``, which can be confusing. Use ``not`` for logical negation
of bools. In the rare case that you really need the bitwise inversion of the underlying ``int``,
convert to int explicitly ``~int(x)``.
18 changes: 17 additions & 1 deletion Objects/boolobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ bool_vectorcall(PyObject *type, PyObject * const*args,

/* Arithmetic operations redefined to return bool if both args are bool. */

static PyObject *
bool_invert(PyObject *v)
{
if (PyErr_WarnEx(PyExc_DeprecationWarning,
"Bitwise inversion '~' on bool is deprecated. 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.",
1) < 0) {
return NULL;
}
return PyLong_Type.tp_as_number->nb_invert(v);
}

static PyObject *
bool_and(PyObject *a, PyObject *b)
{
Expand Down Expand Up @@ -119,7 +135,7 @@ static PyNumberMethods bool_as_number = {
0, /* nb_positive */
0, /* nb_absolute */
0, /* nb_bool */
0, /* nb_invert */
(unaryfunc)bool_invert, /* nb_invert */
0, /* nb_lshift */
0, /* nb_rshift */
bool_and, /* nb_and */
Expand Down