Skip to content

Commit 617529a

Browse files
authored
Turn isna() and notna() into TypeGuards (#339)
* Turn isna() and notna() into TypeGuards * Assert the true/false return of isna/notna on a scalar value * Adjust TypeGuard tests to document limitations of notna/isna type narrowing * Document conflicting MyPy and pyright results * Comment out the tests which produce conflicting results in Mypy and Pyright Keep the tests as comments in order to document the behavior for future reference * Use explicit type annotations per feedback
1 parent f051cd7 commit 617529a

File tree

2 files changed

+75
-22
lines changed

2 files changed

+75
-22
lines changed

pandas-stubs/core/dtypes/missing.pyi

+7-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
from typing import (
2-
Literal,
3-
overload,
4-
)
1+
from typing import overload
52

63
import numpy as np
74
from numpy import typing as npt
@@ -10,12 +7,14 @@ from pandas import (
107
Index,
118
Series,
129
)
10+
from typing_extensions import TypeGuard
1311

1412
from pandas._libs.missing import NAType
1513
from pandas._libs.tslibs import NaTType
1614
from pandas._typing import (
1715
ArrayLike,
1816
Scalar,
17+
ScalarT,
1918
)
2019

2120
isposinf_scalar = ...
@@ -28,9 +27,9 @@ def isna(obj: Series) -> Series[bool]: ...
2827
@overload
2928
def isna(obj: Index | list | ArrayLike) -> npt.NDArray[np.bool_]: ...
3029
@overload
31-
def isna(obj: Scalar) -> bool: ...
32-
@overload
33-
def isna(obj: NaTType | NAType | None) -> Literal[True]: ...
30+
def isna(
31+
obj: Scalar | NaTType | NAType | None,
32+
) -> TypeGuard[NaTType | NAType | None]: ...
3433

3534
isnull = isna
3635

@@ -41,8 +40,6 @@ def notna(obj: Series) -> Series[bool]: ...
4140
@overload
4241
def notna(obj: Index | list | ArrayLike) -> npt.NDArray[np.bool_]: ...
4342
@overload
44-
def notna(obj: Scalar) -> bool: ...
45-
@overload
46-
def notna(obj: NaTType | NAType | None) -> Literal[False]: ...
43+
def notna(obj: ScalarT | NaTType | NAType | None) -> TypeGuard[ScalarT]: ...
4744

4845
notnull = notna

tests/test_pandas.py

+68-12
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from typing import (
66
TYPE_CHECKING,
77
Any,
8-
Literal,
98
Union,
109
)
1110

@@ -17,6 +16,8 @@
1716
import pytest
1817
from typing_extensions import assert_type
1918

19+
from pandas._libs.missing import NAType
20+
from pandas._libs.tslibs import NaTType
2021
from pandas._typing import Scalar
2122

2223
from tests import (
@@ -246,17 +247,72 @@ def test_isna() -> None:
246247
idx2 = pd.Index([1, 2])
247248
check(assert_type(pd.notna(idx2), npt.NDArray[np.bool_]), np.ndarray, np.bool_)
248249

249-
assert check(assert_type(pd.isna(pd.NA), Literal[True]), bool)
250-
assert not check(assert_type(pd.notna(pd.NA), Literal[False]), bool)
251-
252-
assert check(assert_type(pd.isna(pd.NaT), Literal[True]), bool)
253-
assert not check(assert_type(pd.notna(pd.NaT), Literal[False]), bool)
254-
255-
assert check(assert_type(pd.isna(None), Literal[True]), bool)
256-
assert not check(assert_type(pd.notna(None), Literal[False]), bool)
257-
258-
check(assert_type(pd.isna(2.5), bool), bool)
259-
check(assert_type(pd.notna(2.5), bool), bool)
250+
assert check(assert_type(pd.isna(pd.NA), bool), bool)
251+
assert not check(assert_type(pd.notna(pd.NA), bool), bool)
252+
253+
assert check(assert_type(pd.isna(pd.NaT), bool), bool)
254+
assert not check(assert_type(pd.notna(pd.NaT), bool), bool)
255+
256+
assert check(assert_type(pd.isna(None), bool), bool)
257+
assert not check(assert_type(pd.notna(None), bool), bool)
258+
259+
assert not check(assert_type(pd.isna(2.5), bool), bool)
260+
assert check(assert_type(pd.notna(2.5), bool), bool)
261+
262+
# Check TypeGuard type narrowing functionality
263+
# TODO: Due to limitations in TypeGuard spec, the true annotations are not always viable
264+
# and as a result the type narrowing does not always work as it intuitively should
265+
# There is a proposal being floated for a StrictTypeGuard that will have more rigid narrowing semantics
266+
# In the test cases below, a commented out assertion will be included to document the optimal test result
267+
nullable1: str | None | NAType | NaTType = random.choice(
268+
["value", None, pd.NA, pd.NaT]
269+
)
270+
if pd.notna(nullable1):
271+
check(assert_type(nullable1, str), str)
272+
if not pd.isna(nullable1):
273+
# check(assert_type(nullable1, str), str) # TODO: Desired result (see comments above)
274+
check(assert_type(nullable1, Union[str, NaTType, NAType, None]), str)
275+
if pd.isna(nullable1):
276+
assert_type(nullable1, Union[NaTType, NAType, None])
277+
if not pd.notna(nullable1):
278+
# assert_type(nullable1, Union[NaTType, NAType, None]) # TODO: Desired result (see comments above)
279+
assert_type(nullable1, Union[str, NaTType, NAType, None])
280+
281+
nullable2: int | None = random.choice([2, None])
282+
if pd.notna(nullable2):
283+
check(assert_type(nullable2, int), int)
284+
if not pd.isna(nullable2):
285+
# check(assert_type(nullable2, int), int) # TODO: Desired result (see comments above)
286+
check(assert_type(nullable2, Union[int, None]), int)
287+
if pd.isna(nullable2):
288+
# check(assert_type(nullable2, None), type(None)) # TODO: Desired result (see comments above)
289+
check(assert_type(nullable2, Union[NaTType, NAType, None]), type(None))
290+
if not pd.notna(nullable2):
291+
# check(assert_type(nullable2, None), type(None)) # TODO: Desired result (see comments above)
292+
# TODO: MyPy and Pyright produce conflicting results:
293+
# assert_type(nullable2, Union[int, None]) # MyPy result
294+
# assert_type(
295+
# nullable2, Union[int, NaTType, NAType, None]
296+
# ) # Pyright result
297+
pass
298+
299+
nullable3: bool | None | NAType = random.choice([True, None, pd.NA])
300+
if pd.notna(nullable3):
301+
check(assert_type(nullable3, bool), bool)
302+
if not pd.isna(nullable3):
303+
# check(assert_type(nullable3, bool), bool) # TODO: Desired result (see comments above)
304+
check(assert_type(nullable3, Union[bool, NAType, None]), bool)
305+
if pd.isna(nullable3):
306+
# assert_type(nullable3, Union[NAType, None]) # TODO: Desired result (see comments above)
307+
assert_type(nullable3, Union[NaTType, NAType, None])
308+
if not pd.notna(nullable3):
309+
# assert_type(nullable3, Union[NAType, None]) # TODO: Desired result (see comments above)
310+
# TODO: MyPy and Pyright produce conflicting results:
311+
# assert_type(nullable3, Union[bool, NAType, None]) # Mypy result
312+
# assert_type(
313+
# nullable3, Union[bool, NaTType, NAType, None]
314+
# ) # Pyright result
315+
pass
260316

261317

262318
# GH 55

0 commit comments

Comments
 (0)