Skip to content

bpo-35712: Make using NotImplemented in a boolean context issue a dep… #13195

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 6 commits into from
Mar 3, 2020
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
7 changes: 6 additions & 1 deletion Doc/library/constants.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ A small number of constants live in the built-in namespace. They are:
etc.) to indicate that the operation is not implemented with respect to
the other type; may be returned by the in-place binary special methods
(e.g. :meth:`__imul__`, :meth:`__iand__`, etc.) for the same purpose.
Its truth value is true.
It should not be evaluated in a boolean context.

.. note::

Expand All @@ -50,6 +50,11 @@ A small number of constants live in the built-in namespace. They are:
even though they have similar names and purposes.
See :exc:`NotImplementedError` for details on when to use it.

.. versionchanged:: 3.9
Evaluating ``NotImplemented`` in a boolean context is deprecated. While
it currently evaluates as true, it will emit a :exc:`DeprecationWarning`.
It will raise a :exc:`TypeError` in a future version of Python.


.. index:: single: ...; ellipsis literal
.. data:: Ellipsis
Expand Down
9 changes: 7 additions & 2 deletions Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,18 @@ NotImplemented
object is accessed through the built-in name ``NotImplemented``. Numeric methods
and rich comparison methods should return this value if they do not implement the
operation for the operands provided. (The interpreter will then try the
reflected operation, or some other fallback, depending on the operator.) Its
truth value is true.
reflected operation, or some other fallback, depending on the operator.) It
should not be evaluated in a boolean context.

See
:ref:`implementing-the-arithmetic-operations`
for more details.

.. versionchanged:: 3.9
Evaluating ``NotImplemented`` in a boolean context is deprecated. While
it currently evaluates as true, it will emit a :exc:`DeprecationWarning`.
It will raise a :exc:`TypeError` in a future version of Python.


Ellipsis
.. index::
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,12 @@ Deprecated
of Python. For the majority of use cases, users can leverage the Abstract Syntax
Tree (AST) generation and compilation stage, using the :mod:`ast` module.

* Using :data:`NotImplemented` in a boolean context has been deprecated,
as it is almost exclusively the result of incorrect rich comparator
implementations. It will be made a :exc:`TypeError` in a future version
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure that it will be a TypeError. It may be a RuntimeWarning.

The programmer can silence a warning programmatically if it is raised in the code which he cannot change, but he cannot silence a TypeError.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I didn't see this comment before pushing my changes to datamodel/constants, so they still reference TypeError.

In any event, is there some reason to stop at RuntimeWarning? I thought DeprecationWarning was supposed to give all the "code you cannot change" (third party packages) time to update themselves so nothing is using the error-prone code path by the time it becomes an actual error?

I can change it, but I'd prefer this to be an error eventually; RuntimeWarning is used in a few places, so the guidelines aren't clear, but in most cases it seems to boil down to either a mostly harmless thing where the warning tells you you asked Python to do something nonsensical/suboptimal (e.g. bad buffering configuration), and it chose to ignore you and do something valid/more optimal instead, or in the case of asyncio, a case that is probably an error (failing to await a coroutine), but the problem can't be detected until an unpredictable time after the problem occurred, so raising an exception isn't feasible at that point (the same way exceptions in del are suppressed). In most of these cases, Python can stumble along and ignoring the warning doesn't leave you doing anything wrong, just possibly suboptimal. In almost all cases, if NotImplemented is used in a boolean context, and there is no clearly valid way for Python to handle it (what should have delegated or raised TypeError becomes a return of True or False), thus my preference for (eventually) making it an error.

of Python.
(Contributed by Josh Rosenberg in :issue:`35712`.)

* The :mod:`random` module currently accepts any hashable type as a
possible seed value. Unfortunately, some of those types are not
guaranteed to have a deterministic hash value. After Python 3.9,
Expand Down
4 changes: 4 additions & 0 deletions Lib/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ def _gt_from_lt(self, other, NotImplemented=NotImplemented):
def _le_from_lt(self, other, NotImplemented=NotImplemented):
'Return a <= b. Computed by @total_ordering from (a < b) or (a == b).'
op_result = self.__lt__(other)
if op_result is NotImplemented:
return op_result
return op_result or self == other

def _ge_from_lt(self, other, NotImplemented=NotImplemented):
Expand Down Expand Up @@ -136,6 +138,8 @@ def _lt_from_gt(self, other, NotImplemented=NotImplemented):
def _ge_from_gt(self, other, NotImplemented=NotImplemented):
'Return a >= b. Computed by @total_ordering from (a > b) or (a == b).'
op_result = self.__gt__(other)
if op_result is NotImplemented:
return op_result
return op_result or self == other

def _le_from_gt(self, other, NotImplemented=NotImplemented):
Expand Down
6 changes: 3 additions & 3 deletions Lib/ipaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -1398,7 +1398,7 @@ def __str__(self):

def __eq__(self, other):
address_equal = IPv4Address.__eq__(self, other)
if not address_equal or address_equal is NotImplemented:
if address_equal is NotImplemented or not address_equal:
return address_equal
try:
return self.network == other.network
Expand Down Expand Up @@ -2096,7 +2096,7 @@ def __str__(self):

def __eq__(self, other):
address_equal = IPv6Address.__eq__(self, other)
if not address_equal or address_equal is NotImplemented:
if address_equal is NotImplemented or not address_equal:
return address_equal
try:
return self.network == other.network
Expand All @@ -2109,7 +2109,7 @@ def __eq__(self, other):
def __lt__(self, other):
address_less = IPv6Address.__lt__(self, other)
if address_less is NotImplemented:
return NotImplemented
return address_less
try:
return (self.network < other.network or
self.network == other.network and address_less)
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2528,7 +2528,7 @@ def f(): return 7
values = [INT(9), IDX(9),
2.2+3j, Decimal("-21.1"), 12.2, Fraction(5, 2),
[1,2,3], {4,5,6}, {7:8}, (), (9,),
True, False, None, NotImplemented,
True, False, None, Ellipsis,
b'a', b'abc', bytearray(b'a'), bytearray(b'abc'),
'a', 'abc', r'a', r'abc',
f, lambda x: x]
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1666,6 +1666,20 @@ def test_construct_singletons(self):
self.assertRaises(TypeError, tp, 1, 2)
self.assertRaises(TypeError, tp, a=1, b=2)

def test_warning_notimplemented(self):
# Issue #35712: NotImplemented is a sentinel value that should never
# be evaluated in a boolean context (virtually all such use cases
# are a result of accidental misuse implementing rich comparison
# operations in terms of one another).
# For the time being, it will continue to evaluate as truthy, but
# issue a deprecation warning (with the eventual intent to make it
# a TypeError).
self.assertWarns(DeprecationWarning, bool, NotImplemented)
with self.assertWarns(DeprecationWarning):
self.assertTrue(NotImplemented)
with self.assertWarns(DeprecationWarning):
self.assertFalse(not NotImplemented)


class TestBreakpoint(unittest.TestCase):
def setUp(self):
Expand Down
6 changes: 3 additions & 3 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2526,9 +2526,9 @@ def getdict(self):
except TypeError:
pass

# Two essentially featureless objects, just inheriting stuff from
# object.
self.assertEqual(dir(NotImplemented), dir(Ellipsis))
# Two essentially featureless objects, (Ellipsis just inherits stuff
# from object.
self.assertEqual(dir(object()), dir(Ellipsis))

# Nasty test case for proxied objects
class Wrapper(object):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Using :data:`NotImplemented` in a boolean context has been deprecated. Patch
contributed by Josh Rosenberg.
20 changes: 18 additions & 2 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -1673,6 +1673,22 @@ notimplemented_dealloc(PyObject* ignore)
Py_FatalError("deallocating NotImplemented");
}

static int
notimplemented_bool(PyObject *v)
{
if (PyErr_WarnEx(PyExc_DeprecationWarning,
"NotImplemented should not be used in a boolean context",
1) < 0)
{
return -1;
}
return 1;
}

static PyNumberMethods notimplemented_as_number = {
.nb_bool = notimplemented_bool,
};

PyTypeObject _PyNotImplemented_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"NotImplementedType",
Expand All @@ -1683,8 +1699,8 @@ PyTypeObject _PyNotImplemented_Type = {
0, /*tp_getattr*/
0, /*tp_setattr*/
0, /*tp_as_async*/
NotImplemented_repr, /*tp_repr*/
0, /*tp_as_number*/
NotImplemented_repr, /*tp_repr*/
&notimplemented_as_number, /*tp_as_number*/
0, /*tp_as_sequence*/
0, /*tp_as_mapping*/
0, /*tp_hash */
Expand Down