Repository: pandas-dev/pandas
Pull request #55788 (2023-11-01 11:37:39)

DOC: Index docstring to clarify dtype


Patch:
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index 99b6310a80..16d279bb0d 100644
--- a/doc/source/whatsnew/v2.2.0.rst
+++ b/doc/source/whatsnew/v2.2.0.rst
@@ -342,6 +342,7 @@ Timedelta
 Timezones
 ^^^^^^^^^
 - Bug in :class:`AbstractHolidayCalendar` where timezone data was not propagated when computing holiday observances (:issue:`54580`)
+- Bug in :class:`Timestamp` construction with an ambiguous value and a ``pytz`` timezone failing to raise ``pytz.AmbiguousTimeError`` (:issue:`55657`)
 -
 
 Numeric
diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx
index dc1cc906c9..68aaaafd20 100644
--- a/pandas/_libs/tslibs/conversion.pyx
+++ b/pandas/_libs/tslibs/conversion.pyx
@@ -645,7 +645,8 @@ cdef datetime _localize_pydatetime(datetime dt, tzinfo tz):
     """
     try:
         # datetime.replace with pytz may be incorrect result
-        return tz.localize(dt)
+        # TODO: try to respect `fold` attribute
+        return tz.localize(dt, is_dst=None)
     except AttributeError:
         return dt.replace(tzinfo=tz)
 
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
index 0301830df4..ebf4f2d515 100644
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -325,12 +325,12 @@ class Index(IndexOpsMixin, PandasObject):
     Parameters
     ----------
     data : array-like (1-dimensional)
-    dtype : NumPy dtype (default: object)
-        If dtype is None, we find the dtype that best fits the data.
-        If an actual dtype is provided, we coerce to that dtype if it's safe.
-        Otherwise, an error will be raised.
-    copy : bool
-        Make a copy of input ndarray.
+    dtype : str, numpy.dtype, or ExtensionDtype, optional
+        Data type for the output Index. If not specified, this will be
+        inferred from `data`.
+        See the :ref:`user guide <basics.dtypes>` for more usages.
+    copy : bool, default False
+        Copy input data.
     name : object
         Name to be stored in the index.
     tupleize_cols : bool (default: True)


Test patch:
diff --git a/pandas/tests/indexes/datetimes/test_constructors.py b/pandas/tests/indexes/datetimes/test_constructors.py
index 90ddc9b5f6..22353da57d 100644
--- a/pandas/tests/indexes/datetimes/test_constructors.py
+++ b/pandas/tests/indexes/datetimes/test_constructors.py
@@ -1013,6 +1013,36 @@ class TestDatetimeIndex:
         dr2 = DatetimeIndex(list(dr), name="foo", freq="D")
         tm.assert_index_equal(dr, dr2)
 
+    def test_dti_ambiguous_matches_timestamp(self):
+        # GH#47471 check that we get the same raising behavior in the DTI
+        # constructor and Timestamp constructor
+        dtstr = "2013-11-03 01:59:59.999999"
+        dtobj = Timestamp(dtstr).to_pydatetime()
+
+        tz = pytz.timezone("US/Eastern")
+        with pytest.raises(pytz.AmbiguousTimeError, match=dtstr):
+            Timestamp(dtstr, tz=tz)
+        with pytest.raises(pytz.AmbiguousTimeError, match=dtstr):
+            Timestamp(dtobj, tz=tz)
+        with pytest.raises(pytz.AmbiguousTimeError, match=dtstr):
+            DatetimeIndex([dtstr], tz=tz)
+        with pytest.raises(pytz.AmbiguousTimeError, match=dtstr):
+            DatetimeIndex([dtobj], tz=tz)
+
+        tz2 = gettz("US/Eastern")
+        with pytest.raises(pytz.AmbiguousTimeError, match=dtstr):
+            Timestamp(dtstr, tz=tz2)
+        # FIXME: The Timestamp constructor here behaves differently than all
+        #  the other cases bc with dateutil/zoneinfo tzinfos we implicitly
+        #  get fold=0. Having this raise is not important, but having the
+        #  behavior be consistent across cases is.
+        # with pytest.raises(pytz.AmbiguousTimeError, match=dtstr):
+        #    Timestamp(dtobj, tz=tz2)
+        with pytest.raises(pytz.AmbiguousTimeError, match=dtstr):
+            DatetimeIndex([dtstr], tz=tz2)
+        with pytest.raises(pytz.AmbiguousTimeError, match=dtstr):
+            DatetimeIndex([dtobj], tz=tz2)
+
     @pytest.mark.parametrize("tz", [None, "UTC", "US/Pacific"])
     def test_dti_constructor_with_non_nano_dtype(self, tz):
         # GH#55756, GH#54620
diff --git a/pandas/tests/tseries/offsets/test_dst.py b/pandas/tests/tseries/offsets/test_dst.py
index ea4855baa8..b22dc0b330 100644
--- a/pandas/tests/tseries/offsets/test_dst.py
+++ b/pandas/tests/tseries/offsets/test_dst.py
@@ -29,7 +29,10 @@ from pandas._libs.tslibs.offsets import (
     YearBegin,
     YearEnd,
 )
+from pandas.errors import PerformanceWarning
 
+from pandas import DatetimeIndex
+import pandas._testing as tm
 from pandas.util.version import Version
 
 # error: Module has no attribute "__version__"
@@ -83,6 +86,29 @@ class TestDST:
     def _test_offset(self, offset_name, offset_n, tstart, expected_utc_offset):
         offset = DateOffset(**{offset_name: offset_n})
 
+        if (
+            offset_name in ["hour", "minute", "second", "microsecond"]
+            and offset_n == 1
+            and tstart == Timestamp("2013-11-03 01:59:59.999999-0500", tz="US/Eastern")
+        ):
+            # This addition results in an ambiguous wall time
+            err_msg = {
+                "hour": "2013-11-03 01:59:59.999999",
+                "minute": "2013-11-03 01:01:59.999999",
+                "second": "2013-11-03 01:59:01.999999",
+                "microsecond": "2013-11-03 01:59:59.000001",
+            }[offset_name]
+            with pytest.raises(pytz.AmbiguousTimeError, match=err_msg):
+                tstart + offset
+            # While we're here, let's check that we get the same behavior in a
+            #  vectorized path
+            dti = DatetimeIndex([tstart])
+            warn_msg = "Non-vectorized DateOffset"
+            with pytest.raises(pytz.AmbiguousTimeError, match=err_msg):
+                with tm.assert_produces_warning(PerformanceWarning, match=warn_msg):
+                    dti + offset
+            return
+
         t = tstart + offset
         if expected_utc_offset is not None:
             assert get_utc_offset_hours(t) == expected_utc_offset