From 804d7b4d204b6ffb9eed5d64a46671335605647b Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 16 Apr 2026 10:41:27 -0400 Subject: [PATCH 1/7] feat: enable mypy session for db-dtypes --- packages/db-dtypes/db_dtypes/core.py | 14 +++++++------- packages/db-dtypes/db_dtypes/json.py | 4 ++-- .../db-dtypes/db_dtypes/pandas_backports.py | 10 +++++----- packages/db-dtypes/noxfile.py | 18 +++++++++++++++--- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/packages/db-dtypes/db_dtypes/core.py b/packages/db-dtypes/db_dtypes/core.py index 926a11094018..11bcb9552252 100644 --- a/packages/db-dtypes/db_dtypes/core.py +++ b/packages/db-dtypes/db_dtypes/core.py @@ -58,7 +58,7 @@ def __init__(self, values, dtype=None, copy: bool = False): elif copy: values = values.copy() - super().__init__(values=values, dtype=values.dtype) + super().__init__(values=values, dtype=values.dtype) # type: ignore[call-arg] @classmethod def __ndarray(cls, scalars): @@ -164,8 +164,8 @@ def min(self, *, axis: Optional[int] = None, skipna: bool = True, **kwargs): values=self._ndarray, axis=axis, mask=self.isna(), skipna=skipna ) if axis is None or self.ndim == 1: - return self._box_func(result) - return self._from_backing_data(result) + return self._box_func(result) # type: ignore[attr-defined] + return self._from_backing_data(result) # type: ignore[attr-defined] def max(self, *, axis: Optional[int] = None, skipna: bool = True, **kwargs): pandas_backports.numpy_validate_max((), kwargs) @@ -173,8 +173,8 @@ def max(self, *, axis: Optional[int] = None, skipna: bool = True, **kwargs): values=self._ndarray, axis=axis, mask=self.isna(), skipna=skipna ) if axis is None or self.ndim == 1: - return self._box_func(result) - return self._from_backing_data(result) + return self._box_func(result) # type: ignore[attr-defined] + return self._from_backing_data(result) # type: ignore[attr-defined] def median( self, @@ -191,5 +191,5 @@ def median( ) result = pandas_backports.nanmedian(self._ndarray, axis=axis, skipna=skipna) if axis is None or self.ndim == 1: - return self._box_func(result) - return self._from_backing_data(result) + return self._box_func(result) # type: ignore[attr-defined] + return self._from_backing_data(result) # type: ignore[attr-defined] diff --git a/packages/db-dtypes/db_dtypes/json.py b/packages/db-dtypes/db_dtypes/json.py index 37aad83da80c..7f51fbc6088b 100644 --- a/packages/db-dtypes/db_dtypes/json.py +++ b/packages/db-dtypes/db_dtypes/json.py @@ -32,12 +32,12 @@ class JSONDtype(pd.api.extensions.ExtensionDtype): name = "dbjson" @property - def na_value(self) -> pd.NA: + def na_value(self) -> pd.NA: # type: ignore[valid-type] """Default NA value to use for this type.""" return pd.NA @property - def type(self) -> type[str]: + def type(self) -> type[str]: # type: ignore[override] """ Return the scalar type for the array elements. The standard JSON data types can be one of `dict`, `list`, `str`, `int`, `float`, diff --git a/packages/db-dtypes/db_dtypes/pandas_backports.py b/packages/db-dtypes/db_dtypes/pandas_backports.py index 378bb41708f7..c9a658c2006e 100644 --- a/packages/db-dtypes/db_dtypes/pandas_backports.py +++ b/packages/db-dtypes/db_dtypes/pandas_backports.py @@ -26,16 +26,16 @@ pandas_release = packaging.version.parse(pandas.__version__).release # # Create aliases for private methods in case they move in a future version. -nanall = pandas.core.nanops.nanall -nanany = pandas.core.nanops.nanany -nanmax = pandas.core.nanops.nanmax -nanmin = pandas.core.nanops.nanmin +nanall = pandas.core.nanops.nanall # type: ignore[attr-defined] +nanany = pandas.core.nanops.nanany # type: ignore[attr-defined] +nanmax = pandas.core.nanops.nanmax # type: ignore[attr-defined] +nanmin = pandas.core.nanops.nanmin # type: ignore[attr-defined] numpy_validate_all = pandas.compat.numpy.function.validate_all numpy_validate_any = pandas.compat.numpy.function.validate_any numpy_validate_max = pandas.compat.numpy.function.validate_max numpy_validate_min = pandas.compat.numpy.function.validate_min -nanmedian = pandas.core.nanops.nanmedian +nanmedian = pandas.core.nanops.nanmedian # type: ignore[attr-defined] numpy_validate_median = pandas.compat.numpy.function.validate_median diff --git a/packages/db-dtypes/noxfile.py b/packages/db-dtypes/noxfile.py index c7200dce77f1..d8736dca1da3 100644 --- a/packages/db-dtypes/noxfile.py +++ b/packages/db-dtypes/noxfile.py @@ -487,6 +487,18 @@ def core_deps_from_source(session): @nox.session(python=DEFAULT_PYTHON_VERSION) def mypy(session): """Run the type checker.""" - # TODO(https://github.com/googleapis/google-cloud-python/issues/16014): - # Add mypy tests - session.skip("mypy tests are not yet supported") + session.install( + "mypy<1.16.0", + "types-requests", + "types-protobuf", + "pandas-stubs", + ) + session.install("-e", ".") + session.run( + "mypy", + "-p", + "db_dtypes", + "--check-untyped-defs", + "--ignore-missing-imports", + *session.posargs, + ) From 663d7fe269f0ba71882846ae76cfad468e2a413a Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Thu, 16 Apr 2026 11:20:58 -0400 Subject: [PATCH 2/7] Apply suggestion from @gemini-code-assist[bot] Giving pd.NAType a try instead of Any to see if that passes. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/db-dtypes/db_dtypes/json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/db-dtypes/db_dtypes/json.py b/packages/db-dtypes/db_dtypes/json.py index 7f51fbc6088b..9baf0680b4c8 100644 --- a/packages/db-dtypes/db_dtypes/json.py +++ b/packages/db-dtypes/db_dtypes/json.py @@ -32,7 +32,7 @@ class JSONDtype(pd.api.extensions.ExtensionDtype): name = "dbjson" @property - def na_value(self) -> pd.NA: # type: ignore[valid-type] + def na_value(self) -> pd.NAType: """Default NA value to use for this type.""" return pd.NA From 6d602aa875d80b29478f3b4b92d06888e6ba8d7e Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 16 Apr 2026 11:29:13 -0400 Subject: [PATCH 3/7] feat: fix remaining mypy errors in db-dtypes --- packages/db-dtypes/db_dtypes/__init__.py | 10 ++++++---- packages/db-dtypes/db_dtypes/core.py | 21 ++++++++++++++------- packages/db-dtypes/db_dtypes/json.py | 4 ++-- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/db-dtypes/db_dtypes/__init__.py b/packages/db-dtypes/db_dtypes/__init__.py index 0718a2ca4a46..a45dff80081f 100644 --- a/packages/db-dtypes/db_dtypes/__init__.py +++ b/packages/db-dtypes/db_dtypes/__init__.py @@ -55,7 +55,8 @@ class TimeDtype(core.BaseDatetimeDtype): name = time_dtype_name type = datetime.time - def construct_array_type(self): + @classmethod + def construct_array_type(cls): return TimeArray @staticmethod @@ -213,7 +214,8 @@ class DateDtype(core.BaseDatetimeDtype): name = date_dtype_name type = datetime.date - def construct_array_type(self): + @classmethod + def construct_array_type(cls): return DateArray @staticmethod @@ -322,7 +324,7 @@ def __add__(self, other): if isinstance(other, TimeArray): return (other._ndarray - _NPEPOCH) + self._ndarray - return super().__add__(other) + return super().__add__(other) # type: ignore[misc] def __radd__(self, other): return self.__add__(other) @@ -334,7 +336,7 @@ def __sub__(self, other): if isinstance(other, self.__class__): return self._ndarray - other._ndarray - return super().__sub__(other) + return super().__sub__(other) # type: ignore[misc] def _check_python_version(): diff --git a/packages/db-dtypes/db_dtypes/core.py b/packages/db-dtypes/db_dtypes/core.py index 11bcb9552252..ea82839cc762 100644 --- a/packages/db-dtypes/db_dtypes/core.py +++ b/packages/db-dtypes/db_dtypes/core.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional +from typing import Optional, Callable, Any import numpy import pandas @@ -50,6 +50,13 @@ class BaseDatetimeArray(pandas_backports.OpsMixin, _mixins.NDArrayBackedExtensio # https://github.com/pandas-dev/pandas/blob/main/pandas/core/arrays/_mixins.py _internal_fill_value = numpy.datetime64("NaT") + _box_func: Callable[[Any], Any] + _from_backing_data: Callable[[Any], Any] + + @classmethod + def _datetime(cls, value: Any) -> Any: + raise NotImplementedError + def __init__(self, values, dtype=None, copy: bool = False): if not ( isinstance(values, numpy.ndarray) and values.dtype == numpy.dtype(" pd.NAType: + def na_value(self) -> pd.NAType: # type: ignore[name-defined] """Default NA value to use for this type.""" return pd.NA @@ -203,7 +203,7 @@ def __getitem__(self, item): assert item.dtype.kind == "b" return type(self)(self.pa_data.filter(item)) elif isinstance(item, tuple): - item = indexers.unpack_tuple_and_ellipses(item) + item = indexers.unpack_tuple_and_ellipses(item) # type: ignore[attr-defined] if common.is_scalar(item) and not common.is_integer(item): # e.g. "foo" or 2.5 From 04430f65d010c3d59611cd63ed14e9d45674f05b Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Fri, 17 Apr 2026 10:05:12 -0400 Subject: [PATCH 4/7] Update values assignment segregated the steps associated with assigning values to NDArrayBackedExtensionArray versus PandasObject/ExtensionArray to improve clarity and avoid a mypy type hinting issue. --- packages/db-dtypes/db_dtypes/core.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/db-dtypes/db_dtypes/core.py b/packages/db-dtypes/db_dtypes/core.py index ea82839cc762..b48fdb0d254a 100644 --- a/packages/db-dtypes/db_dtypes/core.py +++ b/packages/db-dtypes/db_dtypes/core.py @@ -57,7 +57,7 @@ class BaseDatetimeArray(pandas_backports.OpsMixin, _mixins.NDArrayBackedExtensio def _datetime(cls, value: Any) -> Any: raise NotImplementedError - def __init__(self, values, dtype=None, copy: bool = False): +def __init__(self, values, dtype=None, copy: bool = False): if not ( isinstance(values, numpy.ndarray) and values.dtype == numpy.dtype(" Date: Fri, 17 Apr 2026 10:07:41 -0400 Subject: [PATCH 5/7] Apply suggestion from @chalmerlowe indents function definition. --- packages/db-dtypes/db_dtypes/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/db-dtypes/db_dtypes/core.py b/packages/db-dtypes/db_dtypes/core.py index b48fdb0d254a..ab24e2f93067 100644 --- a/packages/db-dtypes/db_dtypes/core.py +++ b/packages/db-dtypes/db_dtypes/core.py @@ -57,7 +57,7 @@ class BaseDatetimeArray(pandas_backports.OpsMixin, _mixins.NDArrayBackedExtensio def _datetime(cls, value: Any) -> Any: raise NotImplementedError -def __init__(self, values, dtype=None, copy: bool = False): + def __init__(self, values, dtype=None, copy: bool = False): if not ( isinstance(values, numpy.ndarray) and values.dtype == numpy.dtype(" Date: Fri, 17 Apr 2026 10:16:53 -0400 Subject: [PATCH 6/7] Revert changes to super.__init__() Attempting to assign values to the attributes directly resulted in write errors. --- packages/db-dtypes/db_dtypes/core.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/db-dtypes/db_dtypes/core.py b/packages/db-dtypes/db_dtypes/core.py index ab24e2f93067..601768f44a71 100644 --- a/packages/db-dtypes/db_dtypes/core.py +++ b/packages/db-dtypes/db_dtypes/core.py @@ -65,12 +65,12 @@ def __init__(self, values, dtype=None, copy: bool = False): elif copy: values = values.copy() - # 1. Assign the internal attributes required by NDArrayBackedExtensionArray - self._ndarray = values - self._dtype = values.dtype + # We must pass values and dtype to the base constructor. + # Manual assignment (self._ndarray = values) will fail at runtime with + # AttributeError because the base is a Cython-backed 'NDArrayBacked' + # object with non-writable attributes. + super().__init__(values=values, dtype=values.dtype) # type: ignore[call-arg] - # 2. Call super() without arguments to initialize PandasObject/ExtensionArray - super().__init__() @classmethod def __ndarray(cls, scalars): From 8027fd8da944a18e6981ea67dd8a25b5aa392329 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 17 Apr 2026 10:28:37 -0400 Subject: [PATCH 7/7] chore: blacken db_dtypes/core.py --- packages/db-dtypes/db_dtypes/core.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/db-dtypes/db_dtypes/core.py b/packages/db-dtypes/db_dtypes/core.py index 601768f44a71..6baa46cf2d2d 100644 --- a/packages/db-dtypes/db_dtypes/core.py +++ b/packages/db-dtypes/db_dtypes/core.py @@ -65,13 +65,12 @@ def __init__(self, values, dtype=None, copy: bool = False): elif copy: values = values.copy() - # We must pass values and dtype to the base constructor. - # Manual assignment (self._ndarray = values) will fail at runtime with - # AttributeError because the base is a Cython-backed 'NDArrayBacked' + # We must pass values and dtype to the base constructor. + # Manual assignment (self._ndarray = values) will fail at runtime with + # AttributeError because the base is a Cython-backed 'NDArrayBacked' # object with non-writable attributes. super().__init__(values=values, dtype=values.dtype) # type: ignore[call-arg] - @classmethod def __ndarray(cls, scalars): return numpy.array(