Repository: pandas-dev/pandas-stubs (language: python)

Issue:
Use the new TypeIs feature to update the notna/notnull/isna/isnull type guards
I originally contributed the annotations for these methods in https://github.com/pandas-dev/pandas-stubs/pull/339. That PR has a bunch of context about how the fact that `TypeGuard` only narrows in the positive case, while the methods should narrow in both the positive and negative case.

`TypeIs` was recently introduced as a PEP and mypy added support for it, I suspect pyright as well: https://mypy-lang.blogspot.com/2024/04/mypy-110-released.html

This would allow narrowing in the negative case and fix the type narrowing semantics of these methods.



Discussion / hints:

Fix patch:
diff --git a/pandas-stubs/core/dtypes/missing.pyi b/pandas-stubs/core/dtypes/missing.pyi
index 22bf8edd..11319304 100644
--- a/pandas-stubs/core/dtypes/missing.pyi
+++ b/pandas-stubs/core/dtypes/missing.pyi
@@ -10,7 +10,7 @@ from pandas import (
     Index,
     Series,
 )
-from typing_extensions import TypeGuard
+from typing_extensions import TypeIs
 
 from pandas._libs.missing import NAType
 from pandas._libs.tslibs import NaTType
@@ -32,7 +32,7 @@ def isna(obj: Index[Any] | list[Any] | ArrayLike) -> npt.NDArray[np.bool_]: ...
 @overload
 def isna(
     obj: Scalar | NaTType | NAType | None,
-) -> TypeGuard[NaTType | NAType | None]: ...
+) -> TypeIs[NaTType | NAType | None]: ...
 
 isnull = isna
 
@@ -43,6 +43,6 @@ def notna(obj: Series[Any]) -> Series[bool]: ...
 @overload
 def notna(obj: Index[Any] | list[Any] | ArrayLike) -> npt.NDArray[np.bool_]: ...
 @overload
-def notna(obj: ScalarT | NaTType | NAType | None) -> TypeGuard[ScalarT]: ...
+def notna(obj: ScalarT | NaTType | NAType | None) -> TypeIs[ScalarT]: ...
 
 notnull = notna


Test patch:
diff --git a/tests/test_pandas.py b/tests/test_pandas.py
index e7253867..3cd36af7 100644
--- a/tests/test_pandas.py
+++ b/tests/test_pandas.py
@@ -366,60 +366,38 @@ def test_isna() -> None:
     assert check(assert_type(pd.isna(np_nat), bool), bool)
     assert not check(assert_type(pd.notna(np_nat), bool), bool)
 
-    # Check TypeGuard type narrowing functionality
-    # TODO: Due to limitations in TypeGuard spec, the true annotations are not always viable
-    # and as a result the type narrowing does not always work as it intuitively should
-    # There is a proposal being floated for a StrictTypeGuard that will have more rigid narrowing semantics
-    # In the test cases below, a commented out assertion will be included to document the optimal test result
+    # Check TypeIs type narrowing functionality
     nullable1: str | None | NAType | NaTType = random.choice(
         ["value", None, pd.NA, pd.NaT]
     )
     if pd.notna(nullable1):
         check(assert_type(nullable1, str), str)
     if not pd.isna(nullable1):
-        # check(assert_type(nullable1, str), str)  # TODO: Desired result (see comments above)
-        check(assert_type(nullable1, Union[str, NaTType, NAType, None]), str)
+        check(assert_type(nullable1, str), str)
     if pd.isna(nullable1):
         assert_type(nullable1, Union[NaTType, NAType, None])
     if not pd.notna(nullable1):
-        # assert_type(nullable1, Union[NaTType, NAType, None])  # TODO: Desired result (see comments above)
-        assert_type(nullable1, Union[str, NaTType, NAType, None])
+        assert_type(nullable1, Union[NaTType, NAType, None])
 
     nullable2: int | None = random.choice([2, None])
     if pd.notna(nullable2):
         check(assert_type(nullable2, int), int)
     if not pd.isna(nullable2):
-        # check(assert_type(nullable2, int), int)  # TODO: Desired result (see comments above)
-        check(assert_type(nullable2, Union[int, None]), int)
+        check(assert_type(nullable2, int), int)
     if pd.isna(nullable2):
-        # check(assert_type(nullable2, None), type(None))  # TODO: Desired result (see comments above)
-        check(assert_type(nullable2, Union[NaTType, NAType, None]), type(None))
+        check(assert_type(nullable2, None), type(None))
     if not pd.notna(nullable2):
-        # check(assert_type(nullable2, None), type(None))  # TODO: Desired result (see comments above)
-        # TODO: MyPy and Pyright produce conflicting results:
-        # assert_type(nullable2, Union[int, None])  # MyPy result
-        # assert_type(
-        #     nullable2, Union[int, NaTType, NAType, None]
-        # )  # Pyright result
-        pass
+        check(assert_type(nullable2, None), type(None))
 
     nullable3: bool | None | NAType = random.choice([True, None, pd.NA])
     if pd.notna(nullable3):
         check(assert_type(nullable3, bool), bool)
     if not pd.isna(nullable3):
-        # check(assert_type(nullable3, bool), bool)  # TODO: Desired result (see comments above)
-        check(assert_type(nullable3, Union[bool, NAType, None]), bool)
+        check(assert_type(nullable3, bool), bool)
     if pd.isna(nullable3):
-        # assert_type(nullable3, Union[NAType, None])  # TODO: Desired result (see comments above)
-        assert_type(nullable3, Union[NaTType, NAType, None])
+        assert_type(nullable3, Union[NAType, None])
     if not pd.notna(nullable3):
-        # assert_type(nullable3, Union[NAType, None])  # TODO: Desired result (see comments above)
-        # TODO: MyPy and Pyright produce conflicting results:
-        # assert_type(nullable3, Union[bool, NAType, None])  # Mypy result
-        # assert_type(
-        #     nullable3, Union[bool, NaTType, NAType, None]
-        # )  # Pyright result
-        pass
+        assert_type(nullable3, Union[NAType, None])
 
 
 # GH 55