diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 64b9a11b1980d..7c4db6865135a 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -180,7 +180,7 @@ Datetimelike ^^^^^^^^^^^^ - Bug in :class:`DataFrame` and :class:`Series` constructors sometimes dropping nanoseconds from :class:`Timestamp` (resp. :class:`Timedelta`) ``data``, with ``dtype=datetime64[ns]`` (resp. ``timedelta64[ns]``) (:issue:`38032`) - Bug in :meth:`DataFrame.first` and :meth:`Series.first` returning two months for offset one month when first day is last calendar day (:issue:`29623`) -- +- Bug in constructing a :class:`DataFrame` or :class:`Series` with mismatched ``datetime64`` data and ``timedelta64`` dtype, or vice-versa, failing to raise ``TypeError`` (:issue:`38575`) Timedelta ^^^^^^^^^ diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py index d1c16de05ce55..80a1617e0b735 100644 --- a/pandas/core/dtypes/cast.py +++ b/pandas/core/dtypes/cast.py @@ -164,6 +164,14 @@ def maybe_unbox_datetimelike(value: Scalar, dtype: DtypeObj) -> Scalar: value = value.to_datetime64() elif isinstance(value, Timedelta): value = value.to_timedelta64() + + if (isinstance(value, np.timedelta64) and dtype.kind == "M") or ( + isinstance(value, np.datetime64) and dtype.kind == "m" + ): + # numpy allows np.array(dt64values, dtype="timedelta64[ns]") and + # vice-versa, but we do not want to allow this, so we need to + # check explicitly + raise TypeError(f"Cannot cast {repr(value)} to {dtype}") return value diff --git a/pandas/tests/dtypes/cast/test_construct_from_scalar.py b/pandas/tests/dtypes/cast/test_construct_from_scalar.py index a6f97563311a7..4ff3375d1988d 100644 --- a/pandas/tests/dtypes/cast/test_construct_from_scalar.py +++ b/pandas/tests/dtypes/cast/test_construct_from_scalar.py @@ -1,4 +1,5 @@ import numpy as np +import pytest from pandas.core.dtypes.cast import construct_1d_arraylike_from_scalar from pandas.core.dtypes.dtypes import CategoricalDtype @@ -32,3 +33,20 @@ def test_cast_1d_array_like_from_timedelta(): td = Timedelta(1) res = construct_1d_arraylike_from_scalar(td, 2, np.dtype("m8[ns]")) assert res[0] == td + + +def test_cast_1d_array_like_mismatched_datetimelike(): + td = np.timedelta64("NaT", "ns") + dt = np.datetime64("NaT", "ns") + + with pytest.raises(TypeError, match="Cannot cast"): + construct_1d_arraylike_from_scalar(td, 2, dt.dtype) + + with pytest.raises(TypeError, match="Cannot cast"): + construct_1d_arraylike_from_scalar(np.timedelta64(4, "ns"), 2, dt.dtype) + + with pytest.raises(TypeError, match="Cannot cast"): + construct_1d_arraylike_from_scalar(dt, 2, td.dtype) + + with pytest.raises(TypeError, match="Cannot cast"): + construct_1d_arraylike_from_scalar(np.datetime64(4, "ns"), 2, td.dtype) diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index 3d4fbc74d0e6a..5ba38016ee552 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -2962,3 +2962,15 @@ def test_from_timedelta64_scalar_object(self, constructor, request): obj = constructor(td64, dtype=object) assert isinstance(get1(obj), np.timedelta64) + + @pytest.mark.parametrize("cls", [np.datetime64, np.timedelta64]) + def test_from_scalar_datetimelike_mismatched(self, constructor, cls): + scalar = cls("NaT", "ns") + dtype = {np.datetime64: "m8[ns]", np.timedelta64: "M8[ns]"}[cls] + + with pytest.raises(TypeError, match="Cannot cast"): + constructor(scalar, dtype=dtype) + + scalar = cls(4, "ns") + with pytest.raises(TypeError, match="Cannot cast"): + constructor(scalar, dtype=dtype)