Repository: pandas-dev/pandas
Pull request: BUG: NaT.__cmp__(invalid) should raise TypeError
- [x] closes #35046
- [x] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry


Problem statement:
BUG/API Should pd.NaT < "foo" raise?
If we treat NaT as a datetime, we would expect pd.NaT == "foo" to return False, != to return True, and for the inequalities to raise:

```
>>> np.datetime64("NaT", "ns") < "foo"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'numpy.ndarray' and 'str'

>>> np.nan < "foo"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'float' and 'str'

>>> pd.NaT < "foo"
False
```

It isn't obvious to me that the existing behavior is intentional.  If we change this to raise, we can simplify `NaT.__richcmp__` a bit.

Hints from issue discussion:
yeah this looks suspect, should raise

Commit message: whatsnew

Patch to review:
diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst
index b16ca0a80c5b4..cc2befc257c86 100644
--- a/doc/source/whatsnew/v1.2.0.rst
+++ b/doc/source/whatsnew/v1.2.0.rst
@@ -59,7 +59,7 @@ Categorical
 
 Datetimelike
 ^^^^^^^^^^^^
--
+- Bug in ``NaT`` comparisons failing to raise ``TypeError`` on invalid inequality comparisons (:issue:`35046`)
 -
 
 Timedelta
diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx
index 73df51832d700..da1084bd8558d 100644
--- a/pandas/_libs/tslibs/nattype.pyx
+++ b/pandas/_libs/tslibs/nattype.pyx
@@ -107,30 +107,26 @@ cdef class _NaT(datetime):
     __array_priority__ = 100
 
     def __richcmp__(_NaT self, object other, int op):
-        cdef:
-            int ndim = getattr(other, "ndim", -1)
+        if util.is_datetime64_object(other) or PyDateTime_Check(other):
+            # We treat NaT as datetime-like for this comparison
+            return _nat_scalar_rules[op]
 
-        if ndim == -1:
+        elif util.is_timedelta64_object(other) or PyDelta_Check(other):
+            # We treat NaT as timedelta-like for this comparison
             return _nat_scalar_rules[op]
 
         elif util.is_array(other):
-            result = np.empty(other.shape, dtype=np.bool_)
-            result.fill(_nat_scalar_rules[op])
+            if other.dtype.kind in "mM":
+                result = np.empty(other.shape, dtype=np.bool_)
+                result.fill(_nat_scalar_rules[op])
+            elif other.dtype.kind == "O":
+                result = np.array([PyObject_RichCompare(self, x, op) for x in other])
+            else:
+                return NotImplemented
             return result
 
-        elif ndim == 0:
-            if util.is_datetime64_object(other):
-                return _nat_scalar_rules[op]
-            else:
-                raise TypeError(
-                    f"Cannot compare type {type(self).__name__} "
-                    f"with type {type(other).__name__}"
-                )
-
-        # Note: instead of passing "other, self, _reverse_ops[op]", we observe
-        # that `_nat_scalar_rules` is invariant under `_reverse_ops`,
-        # rendering it unnecessary.
-        return PyObject_RichCompare(other, self, op)
+
+        return NotImplemented
 
     def __add__(self, other):
         if self is not c_NaT:
diff --git a/pandas/tests/scalar/test_nat.py b/pandas/tests/scalar/test_nat.py
index e1e2ea1a5cec8..72abd490125ce 100644
--- a/pandas/tests/scalar/test_nat.py
+++ b/pandas/tests/scalar/test_nat.py
@@ -513,11 +513,67 @@ def test_to_numpy_alias():
     assert isna(expected) and isna(result)
 
 
-@pytest.mark.parametrize("other", [Timedelta(0), Timestamp(0)])
+@pytest.mark.parametrize(
+    "other",
+    [
+        Timedelta(0),
+        Timedelta(0).to_pytimedelta(),
+        pytest.param(
+            Timedelta(0).to_timedelta64(),
+            marks=pytest.mark.xfail(
+                reason="td64 doesnt return NotImplemented, see numpy#17017"
+            ),
+        ),
+        Timestamp(0),
+        Timestamp(0).to_pydatetime(),
+        pytest.param(
+            Timestamp(0).to_datetime64(),
+            marks=pytest.mark.xfail(
+                reason="dt64 doesnt return NotImplemented, see numpy#17017"
+            ),
+        ),
+        Timestamp(0).tz_localize("UTC"),
+        NaT,
+    ],
+)
 def test_nat_comparisons(compare_operators_no_eq_ne, other):
     # GH 26039
-    assert getattr(NaT, compare_operators_no_eq_ne)(other) is False
-    assert getattr(other, compare_operators_no_eq_ne)(NaT) is False
+    opname = compare_operators_no_eq_ne
+
+    assert getattr(NaT, opname)(other) is False
+
+    op = getattr(operator, opname.strip("_"))
+    assert op(NaT, other) is False
+    assert op(other, NaT) is False
+
+
+@pytest.mark.parametrize("other", [np.timedelta64(0, "ns"), np.datetime64("now", "ns")])
+def test_nat_comparisons_numpy(other):
+    # Once numpy#17017 is fixed and the xfailed cases in test_nat_comparisons
+    #  pass, this test can be removed
+    assert not NaT == other
+    assert NaT != other
+    assert not NaT < other
+    assert not NaT > other
+    assert not NaT <= other
+    assert not NaT >= other
+
+
+@pytest.mark.parametrize("other", ["foo", 2, 2.0])
+def test_nat_comparisons_invalid(other):
+
+    assert not NaT == other
+    assert not other == NaT
+
+    assert NaT != other
+    assert other != NaT
+
+    for op in [operator.le, operator.lt, operator.ge, operator.gt]:
+        with pytest.raises(TypeError):
+            op(NaT, other)
+
+        with pytest.raises(TypeError):
+            op(other, NaT)
 
 
 @pytest.mark.parametrize(


Code review comments:
diff_hunk: @@ -513,11 +513,67 @@ def test_to_numpy_alias():
     assert isna(expected) and isna(result)
 
 
-@pytest.mark.parametrize("other", [Timedelta(0), Timestamp(0)])
+@pytest.mark.parametrize(
+    "other",
+    [
+        Timedelta(0),
+        Timedelta(0).to_pytimedelta(),
+        pytest.param(
+            Timedelta(0).to_timedelta64(),
+            marks=pytest.mark.xfail(
+                reason="td64 doesnt return NotImplemented, see numpy#17017"
+            ),
+        ),
+        Timestamp(0),
+        Timestamp(0).to_pydatetime(),
+        pytest.param(
+            Timestamp(0).to_datetime64(),
+            marks=pytest.mark.xfail(
+                reason="dt64 doesnt return NotImplemented, see numpy#17017"
+            ),
+        ),
+        Timestamp(0).tz_localize("UTC"),
+        NaT,
+    ],
+)
 def test_nat_comparisons(compare_operators_no_eq_ne, other):
     # GH 26039
-    assert getattr(NaT, compare_operators_no_eq_ne)(other) is False
-    assert getattr(other, compare_operators_no_eq_ne)(NaT) is False
+    opname = compare_operators_no_eq_ne
+
+    assert getattr(NaT, opname)(other) is False
+
+    op = getattr(operator, opname.strip("_"))
+    assert op(NaT, other) is False
+    assert op(other, NaT) is False
+
+
+@pytest.mark.parametrize("other", [np.timedelta64(0, "ns"), np.datetime64("now", "ns")])
+def test_nat_comparisons_numpy(other):
+    # Once numpy#17017 is fixed and the xfailed cases in test_nat_comparisons
+    #  pass, this test can be removed
+    assert not NaT == other
+    assert NaT != other
+    assert not NaT < other
+    assert not NaT > other
+    assert not NaT <= other
+    assert not NaT >= other
+
+
+@pytest.mark.parametrize("other", ["foo", 2, 2.0])
+def test_nat_comparisons_invalid(other):
+
+    assert not NaT == other
+    assert not other == NaT
+
+    assert NaT != other
+    assert other != NaT
+
+    for op in [operator.le, operator.lt, operator.ge, operator.gt]:
path: pandas/tests/scalar/test_nat.py
text: @user1:
Can you parametrize these instead of looping? Makes future debugging easier