Repository: python/cpython
Pull request: gh-115808: Add ``is_none`` and ``is_not_none`` operators
<!--
Thanks for your contribution!
Please read this comment in its entirety. It's quite important.

# Pull Request title

It should be in the following format:

```
gh-NNNNN: Summary of the changes made
```

Where: gh-NNNNN refers to the GitHub issue number.

Most PRs will require an issue number. Trivial changes, like fixing a typo, do not need an issue.

# Backport Pull Request title

If this is a backport PR (PR made against branches other than `main`),
please ensure that the PR title is in the following format:

```
[X.Y] <title from the original PR> (GH-NNNN)
```

Where: [X.Y] is the branch name, e.g. [3.6].

GH-NNNN refers to the PR number from `main`.

-->


This PR adds an `is_none` operator and an appropriate test.
I hope I did not forget to update/reference it anywhere else.

Fixes #115808 

<!-- gh-issue-number: gh-115808 -->
* Issue: gh-115808
<!-- /gh-issue-number -->


Problem statement:
Add operator.is_none()
# Feature or enhancement

### Proposal:

With floating point data, the `float('NaN')` special value is commonly used as a placeholder for missing values.  We provide `math.isnan` to support stripping out those values prior to sorting or summation:

```python
>>> from math import isnan
>>> from itertools import filterfalse
>>> data = [3.3, float('nan'), 1.1, 2.2]
>>> sorted(filterfalse(isnan, data))
[1.1, 2.2, 3.3]
```

For non-float data, the `None` special value is commonly used as a placeholder for missing values.  I propose a new function in the operator module analogous to `math.isnan()`:

```pycon
>>> data = ['c', None, 'a', 'b']
>>> sorted(filterfalse(is_none, data))
['a', 'b', 'c']
```

This helps fulfill a primary use case for the operator module which is to support functional programming with `map()`, `filter()`, etc.

### Has this already been discussed elsewhere?

No response given

### Links to previous discussion of this feature:

_No response_

<!-- gh-linked-prs -->
### Linked PRs
* gh-115814
<!-- /gh-linked-prs -->

Hints from issue discussion:
For convenience, it might also be nice to have `is_not_none()`.   The matches what would be done in a typical list comprehension: `cleaned = [x for x in data if x is not None]`.   Also, it would save the need to know about *filterfalse()* in itertools module which is less well known that builtin functions.  A simple builtin *filter()* call will suffice: `mean(filter(is_not_none, data))`.
> For convenience, it might also be nice to have `is_not_none()`

Agreed -- I was going to suggest this. It also fits the general pattern of the `operator` module: we have both `is_` and `is_not`, both `truth` and `not_`, etc.

Commit message: Add is_not_none operator

Patch to review:
diff --git a/Doc/library/operator.rst b/Doc/library/operator.rst
index a9a6026af406fe..e8e71068dd99eb 100644
--- a/Doc/library/operator.rst
+++ b/Doc/library/operator.rst
@@ -80,6 +80,20 @@ truth tests, identity tests, and boolean operations:
    Return ``a is not b``.  Tests object identity.
 
 
+.. function:: is_none(a)
+
+   Return ``a is None``.  Tests object identity.
+
+   .. versionadded:: 3.14
+
+
+.. function:: is_not_none(a)
+
+   Return ``a is not None``.  Tests object identity.
+
+   .. versionadded:: 3.14
+
+
 The mathematical and bitwise operations are the most numerous:
 
 
@@ -405,6 +419,10 @@ Python syntax and the functions in the :mod:`operator` module.
 +-----------------------+-------------------------+---------------------------------------+
 | Identity              | ``a is not b``          | ``is_not(a, b)``                      |
 +-----------------------+-------------------------+---------------------------------------+
+| Identity              | ``a is None``           | ``is_none(a)``                        |
++-----------------------+-------------------------+---------------------------------------+
+| Identity              | ``a is not None``       | ``is_not_none(a)``                    |
++-----------------------+-------------------------+---------------------------------------+
 | Indexed Assignment    | ``obj[k] = v``          | ``setitem(obj, k, v)``                |
 +-----------------------+-------------------------+---------------------------------------+
 | Indexed Deletion      | ``del obj[k]``          | ``delitem(obj, k)``                   |
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index aecc7cabd0d1f5..b0b22a5671cb93 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -124,6 +124,15 @@ Add notes for JSON serialization errors that allow to identify the source
 of the error.
 (Contributed by Serhiy Storchaka in :gh:`122163`.)
 
+operator
+--------
+
+* Two new functions ``operator.is_none`` and ``operator.is_not_none``
+  have been added, such that ``operator.is_none(obj)`` is equivalent
+  to ``obj is None`` and ``operator.is_not_none(obj)`` is equivalent
+  to ``obj is not None``.
+  (Contributed by Raymond Hettinger and Nico Mexis in :gh:`115808`.)
+
 os
 --
 
diff --git a/Lib/operator.py b/Lib/operator.py
index 6d2a762bc95b6d..1b765522f85949 100644
--- a/Lib/operator.py
+++ b/Lib/operator.py
@@ -14,8 +14,8 @@
            'delitem', 'eq', 'floordiv', 'ge', 'getitem', 'gt', 'iadd', 'iand',
            'iconcat', 'ifloordiv', 'ilshift', 'imatmul', 'imod', 'imul',
            'index', 'indexOf', 'inv', 'invert', 'ior', 'ipow', 'irshift',
-           'is_', 'is_not', 'isub', 'itemgetter', 'itruediv', 'ixor', 'le',
-           'length_hint', 'lshift', 'lt', 'matmul', 'methodcaller', 'mod',
+           'is_', 'is_none', 'is_not', 'is_not_none', 'isub', 'itemgetter', 'itruediv',
+           'ixor', 'le', 'length_hint', 'lshift', 'lt', 'matmul', 'methodcaller', 'mod',
            'mul', 'ne', 'neg', 'not_', 'or_', 'pos', 'pow', 'rshift',
            'setitem', 'sub', 'truediv', 'truth', 'xor']
 
@@ -66,6 +66,14 @@ def is_not(a, b):
     "Same as a is not b."
     return a is not b
 
+def is_none(a):
+    "Same as a is None."
+    return a is None
+
+def is_not_none(a):
+    "Same as a is not None."
+    return a is not None
+
 # Mathematical/Bitwise Operations *********************************************#
 
 def abs(a):
diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py
index f8eac8dc002636..812d46482e238a 100644
--- a/Lib/test/test_operator.py
+++ b/Lib/test/test_operator.py
@@ -347,6 +347,26 @@ def test_is_not(self):
         self.assertFalse(operator.is_not(a, b))
         self.assertTrue(operator.is_not(a,c))
 
+    def test_is_none(self):
+        operator = self.module
+        a = 'xyzpdq'
+        b = ''
+        c = None
+        self.assertRaises(TypeError, operator.is_none)
+        self.assertFalse(operator.is_none(a))
+        self.assertFalse(operator.is_none(b))
+        self.assertTrue(operator.is_none(c))
+
+    def test_is_not_none(self):
+        operator = self.module
+        a = 'xyzpdq'
+        b = ''
+        c = None
+        self.assertRaises(TypeError, operator.is_not_none)
+        self.assertTrue(operator.is_not_none(a))
+        self.assertTrue(operator.is_not_none(b))
+        self.assertFalse(operator.is_not_none(c))
+
     def test_attrgetter(self):
         operator = self.module
         class A:
diff --git a/Misc/NEWS.d/next/Library/2024-02-22-10-12-59.gh-issue-115808.F2g2Ku.rst b/Misc/NEWS.d/next/Library/2024-02-22-10-12-59.gh-issue-115808.F2g2Ku.rst
new file mode 100644
index 00000000000000..0fe6a336bdf95b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-02-22-10-12-59.gh-issue-115808.F2g2Ku.rst
@@ -0,0 +1 @@
+Add :func:`operator.is_none` and :func:`operator.is_not_none` functions.
diff --git a/Modules/_operator.c b/Modules/_operator.c
index 793a2d0dfc6443..92cf2342fe86bd 100644
--- a/Modules/_operator.c
+++ b/Modules/_operator.c
@@ -728,6 +728,34 @@ _operator_is_not_impl(PyObject *module, PyObject *a, PyObject *b)
     return Py_NewRef(result);
 }
 
+/*[clinic input]
+_operator.is_none = _operator.neg
+
+Same as a is None.
+[clinic start generated code]*/
+
+static PyObject *
+_operator_is_none(PyObject *module, PyObject *a)
+/*[clinic end generated code: output=07159cc102261dec input=0448b38af7b8533d]*/
+{
+    PyObject *result = Py_IsNone(a) ? Py_True : Py_False;
+    return Py_NewRef(result);
+}
+
+/*[clinic input]
+_operator.is_not_none = _operator.neg
+
+Same as a is not None.
+[clinic start generated code]*/
+
+static PyObject *
+_operator_is_not_none(PyObject *module, PyObject *a)
+/*[clinic end generated code: output=b0168a51451d9140 input=7587f38ebac51688]*/
+{
+    PyObject *result = Py_IsNotNone(a) ? Py_True : Py_False;
+    return Py_NewRef(result);
+}
+
 /* compare_digest **********************************************************/
 
 /*
@@ -916,6 +944,8 @@ static struct PyMethodDef operator_methods[] = {
     _OPERATOR_COUNTOF_METHODDEF
     _OPERATOR_IS__METHODDEF
     _OPERATOR_IS_NOT_METHODDEF
+    _OPERATOR_IS_NONE_METHODDEF
+    _OPERATOR_IS_NOT_NONE_METHODDEF
     _OPERATOR_INDEX_METHODDEF
     _OPERATOR_ADD_METHODDEF
     _OPERATOR_SUB_METHODDEF
diff --git a/Modules/clinic/_operator.c.h b/Modules/clinic/_operator.c.h
index 08615d690922a1..48a8ea8c3379ab 100644
--- a/Modules/clinic/_operator.c.h
+++ b/Modules/clinic/_operator.c.h
@@ -1393,6 +1393,24 @@ _operator_is_not(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
     return return_value;
 }
 
+PyDoc_STRVAR(_operator_is_none__doc__,
+"is_none($module, a, /)\n"
+"--\n"
+"\n"
+"Same as a is None.");
+
+#define _OPERATOR_IS_NONE_METHODDEF    \
+    {"is_none", (PyCFunction)_operator_is_none, METH_O, _operator_is_none__doc__},
+
+PyDoc_STRVAR(_operator_is_not_none__doc__,
+"is_not_none($module, a, /)\n"
+"--\n"
+"\n"
+"Same as a is not None.");
+
+#define _OPERATOR_IS_NOT_NONE_METHODDEF    \
+    {"is_not_none", (PyCFunction)_operator_is_not_none, METH_O, _operator_is_not_none__doc__},
+
 PyDoc_STRVAR(_operator_length_hint__doc__,
 "length_hint($module, obj, default=0, /)\n"
 "--\n"
@@ -1489,4 +1507,4 @@ _operator__compare_digest(PyObject *module, PyObject *const *args, Py_ssize_t na
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=ddbba2cd943571eb input=a9049054013a1b77]*/
+/*[clinic end generated code: output=972e2543c4fcf1ba input=a9049054013a1b77]*/


Code review comments:
diff_hunk: @@ -742,6 +742,20 @@ _operator_is_none(PyObject *module, PyObject *a)
     return Py_NewRef(result);
 }
 
+/*[clinic input]
+_operator.is_not_none = _operator.neg
+
+Same as a is not None.
+[clinic start generated code]*/
+
+static PyObject *
+_operator_is_not_none(PyObject *module, PyObject *a)
+/*[clinic end generated code: output=b0168a51451d9140 input=7587f38ebac51688]*/
+{
+    PyObject *result = Py_IsNotNone(a) ? Py_True : Py_False;
path: Modules/_operator.c
text: @user1:
```suggestion
    PyObject *result = Py_IsNone(a) ? Py_False : Py_True;
```