Repository: pandas-dev/pandas
Pull request #55889 (2023-11-09 00:32:33)

REF: Mess with data less outside of MPLPlot.__init__
- [ ] closes #xxxx (Replace xxxx with the GitHub issue number)
- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.



Patch:
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index 2920c20119..efa4a52993 100644
--- a/doc/source/whatsnew/v2.2.0.rst
+++ b/doc/source/whatsnew/v2.2.0.rst
@@ -101,6 +101,7 @@ Other enhancements
 - :func:`tseries.api.guess_datetime_format` is now part of the public API (:issue:`54727`)
 - :meth:`ExtensionArray._explode` interface method added to allow extension type implementations of the ``explode`` method (:issue:`54833`)
 - :meth:`ExtensionArray.duplicated` added to allow extension type implementations of the ``duplicated`` method (:issue:`55255`)
+- Allow passing ``read_only``, ``data_only`` and ``keep_links`` arguments to openpyxl using ``engine_kwargs`` of :func:`read_excel` (:issue:`55027`)
 - DataFrame.apply now allows the usage of numba (via ``engine="numba"``) to JIT compile the passed function, allowing for potential speedups (:issue:`54666`)
 - Implement masked algorithms for :meth:`Series.value_counts` (:issue:`54984`)
 - Improved error message when constructing :class:`Period` with invalid offsets such as "QS" (:issue:`55785`)
@@ -337,6 +338,7 @@ Datetimelike
 - Bug in :func:`concat` raising ``AttributeError`` when concatenating all-NA DataFrame with :class:`DatetimeTZDtype` dtype DataFrame. (:issue:`52093`)
 - Bug in :func:`testing.assert_extension_array_equal` that could use the wrong unit when comparing resolutions (:issue:`55730`)
 - Bug in :func:`to_datetime` and :class:`DatetimeIndex` when passing a list of mixed-string-and-numeric types incorrectly raising (:issue:`55780`)
+- Bug in :func:`to_datetime` and :class:`DatetimeIndex` when passing mixed-type objects with a mix of timezones or mix of timezone-awareness failing to raise ``ValueError`` (:issue:`55693`)
 - Bug in :meth:`DatetimeIndex.union` returning object dtype for tz-aware indexes with the same timezone but different units (:issue:`55238`)
 - Bug in :meth:`Index.is_monotonic_increasing` and :meth:`Index.is_monotonic_decreasing` always caching :meth:`Index.is_unique` as ``True`` when first value in index is ``NaT`` (:issue:`55755`)
 - Bug in :meth:`Index.view` to a datetime64 dtype with non-supported resolution incorrectly raising (:issue:`55710`)
diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx
index ad043d1695..72faf1757a 100644
--- a/pandas/_libs/tslib.pyx
+++ b/pandas/_libs/tslib.pyx
@@ -72,6 +72,7 @@ from pandas._libs.tslibs.nattype cimport (
     c_nat_strings as nat_strings,
 )
 from pandas._libs.tslibs.timestamps cimport _Timestamp
+from pandas._libs.tslibs.timezones cimport tz_compare
 
 from pandas._libs.tslibs import (
     Resolution,
@@ -488,9 +489,11 @@ cpdef array_to_datetime(
             elif PyDate_Check(val):
                 iresult[i] = pydate_to_dt64(val, &dts, reso=creso)
                 check_dts_bounds(&dts, creso)
+                state.found_other = True
 
             elif is_datetime64_object(val):
                 iresult[i] = get_datetime64_nanos(val, creso)
+                state.found_other = True
 
             elif is_integer_object(val) or is_float_object(val):
                 # these must be ns unit by-definition
@@ -500,6 +503,7 @@ cpdef array_to_datetime(
                 else:
                     # we now need to parse this as if unit='ns'
                     iresult[i] = cast_from_unit(val, "ns", out_reso=creso)
+                    state.found_other = True
 
             elif isinstance(val, str):
                 # string
@@ -535,6 +539,7 @@ cpdef array_to_datetime(
                     # Add a marker for naive string, to track if we are
                     # parsing mixed naive and aware strings
                     out_tzoffset_vals.add("naive")
+                    state.found_naive_str = True
 
             else:
                 raise TypeError(f"{type(val)} is not convertible to datetime")
@@ -558,9 +563,29 @@ cpdef array_to_datetime(
         is_same_offsets = len(out_tzoffset_vals) == 1
         if not is_same_offsets:
             return _array_to_datetime_object(values, errors, dayfirst, yearfirst)
+        elif state.found_naive or state.found_other:
+            # e.g. test_to_datetime_mixed_awareness_mixed_types
+            raise ValueError("Cannot mix tz-aware with tz-naive values")
+        elif tz_out is not None:
+            # GH#55693
+            tz_offset = out_tzoffset_vals.pop()
+            tz_out2 = timezone(timedelta(seconds=tz_offset))
+            if not tz_compare(tz_out, tz_out2):
+                # e.g. test_to_datetime_mixed_tzs_mixed_types
+                raise ValueError(
+                    "Mixed timezones detected. pass utc=True in to_datetime "
+                    "or tz='UTC' in DatetimeIndex to convert to a common timezone."
+                )
+            # e.g. test_to_datetime_mixed_types_matching_tzs
         else:
             tz_offset = out_tzoffset_vals.pop()
             tz_out = timezone(timedelta(seconds=tz_offset))
+    elif not utc_convert:
+        if tz_out and (state.found_other or state.found_naive_str):
+            # found_other indicates a tz-naive int, float, dt64, or date
+            # e.g. test_to_datetime_mixed_awareness_mixed_types
+            raise ValueError("Cannot mix tz-aware with tz-naive values")
+
     return result, tz_out
 
 
diff --git a/pandas/_libs/tslibs/strptime.pxd b/pandas/_libs/tslibs/strptime.pxd
index fd8cafeaef..dd8936f080 100644
--- a/pandas/_libs/tslibs/strptime.pxd
+++ b/pandas/_libs/tslibs/strptime.pxd
@@ -14,8 +14,11 @@ cdef bint parse_today_now(
 
 cdef class DatetimeParseState:
     cdef:
+        # See comments describing these attributes in the __cinit__ method
         bint found_tz
         bint found_naive
+        bint found_naive_str
+        bint found_other
         bint creso_ever_changed
         NPY_DATETIMEUNIT creso
 
diff --git a/pandas/_libs/tslibs/strptime.pyx b/pandas/_libs/tslibs/strptime.pyx
index fc44d3aad4..c8fd95be34 100644
--- a/pandas/_libs/tslibs/strptime.pyx
+++ b/pandas/_libs/tslibs/strptime.pyx
@@ -242,8 +242,15 @@ cdef _get_format_regex(str fmt):
 
 cdef class DatetimeParseState:
     def __cinit__(self, NPY_DATETIMEUNIT creso=NPY_DATETIMEUNIT.NPY_FR_ns):
+        # found_tz and found_naive are specifically about datetime/Timestamp
+        #  objects with and without tzinfos attached.
         self.found_tz = False
         self.found_naive = False
+        # found_naive_str refers to a string that was parsed to a timezone-naive
+        #  datetime.
+        self.found_naive_str = False
+        self.found_other = False
+
         self.creso = creso
         self.creso_ever_changed = False
 
diff --git a/pandas/io/excel/_openpyxl.py b/pandas/io/excel/_openpyxl.py
index ca7e84f7d6..c546443868 100644
--- a/pandas/io/excel/_openpyxl.py
+++ b/pandas/io/excel/_openpyxl.py
@@ -567,12 +567,11 @@ class OpenpyxlReader(BaseExcelReader["Workbook"]):
     ) -> Workbook:
         from openpyxl import load_workbook
 
+        default_kwargs = {"read_only": True, "data_only": True, "keep_links": False}
+
         return load_workbook(
             filepath_or_buffer,
-            read_only=True,
-            data_only=True,
-            keep_links=False,
-            **engine_kwargs,
+            **(default_kwargs | engine_kwargs),
         )
 
     @property
diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py
index 05cf151992..d59220a1f9 100644
--- a/pandas/plotting/_matplotlib/core.py
+++ b/pandas/plotting/_matplotlib/core.py
@@ -89,11 +89,15 @@ if TYPE_CHECKING:
 
     from pandas._typing import (
         IndexLabel,
+        NDFrameT,
         PlottingOrientation,
         npt,
     )
 
-    from pandas import Series
+    from pandas import (
+        PeriodIndex,
+        Series,
+    )
 
 
 def _color_in_style(style: str) -> bool:
@@ -161,8 +165,6 @@ class MPLPlot(ABC):
     ) -> None:
         import matplotlib.pyplot as plt
 
-        self.data = data
-
         # if users assign an empty list or tuple, raise `ValueError`
         # similar to current `df.box` and `df.hist` APIs.
         if by in ([], ()):
@@ -193,9 +195,11 @@ class MPLPlot(ABC):
 
         self.kind = kind
 
-        self.subplots = self._validate_subplots_kwarg(subplots)
+        self.subplots = type(self)._validate_subplots_kwarg(
+            subplots, data, kind=self._kind
+        )
 
-        self.sharex = self._validate_sharex(sharex, ax, by)
+        self.sharex = type(self)._validate_sharex(sharex, ax, by)
         self.sharey = sharey
         self.figsize = figsize
         self.layout = layout
@@ -245,10 +249,11 @@ class MPLPlot(ABC):
         # parse errorbar input if given
         xerr = kwds.pop("xerr", None)
         yerr = kwds.pop("yerr", None)
-        self.errors = {
-            kw: self._parse_errorbars(kw, err)
-            for kw, err in zip(["xerr", "yerr"], [xerr, yerr])
-        }
+        nseries = self._get_nseries(data)
+        xerr, data = type(self)._parse_errorbars("xerr", xerr, data, nseries)
+        yerr, data = type(self)._parse_errorbars("yerr", yerr, data, nseries)
+        self.errors = {"xerr": xerr, "yerr": yerr}
+        self.data = data
 
         if not isinstance(secondary_y, (bool, tuple, list, np.ndarray, ABCIndex)):
             secondary_y = [secondary_y]
@@ -271,7 +276,8 @@ class MPLPlot(ABC):
         self._validate_color_args()
 
     @final
-    def _validate_sharex(self, sharex: bool | None, ax, by) -> bool:
+    @staticmethod
+    def _validate_sharex(sharex: bool | None, ax, by) -> bool:
         if sharex is None:
             # if by is defined, subplots are used and sharex should be False
             if ax is None and by is None:  # pylint: disable=simplifiable-if-statement
@@ -285,8 +291,9 @@ class MPLPlot(ABC):
         return bool(sharex)
 
     @final
+    @staticmethod
     def _validate_subplots_kwarg(
-        self, subplots: bool | Sequence[Sequence[str]]
+        subplots: bool | Sequence[Sequence[str]], data: Series | DataFrame, kind: str
     ) -> bool | list[tuple[int, ...]]:
         """
         Validate the subplots parameter
@@ -323,18 +330,18 @@ class MPLPlot(ABC):
             "area",
             "pie",
         )
-        if self._kind not in supported_kinds:
+        if kind not in supported_kinds:
             raise ValueError(
                 "When subplots is an iterable, kind must be "
-                f"one of {', '.join(supported_kinds)}. Got {self._kind}."
+                f"one of {', '.join(supported_kinds)}. Got {kind}."
             )
 
-        if isinstance(self.data, ABCSeries):
+        if isinstance(data, ABCSeries):
             raise NotImplementedError(
                 "An iterable subplots for a Series is not supported."
             )
 
-        columns = self.data.columns
+        columns = data.columns
         if isinstance(columns, ABCMultiIndex):
             raise NotImplementedError(
                 "An iterable subplots for a DataFrame with a MultiIndex column "
@@ -442,18 +449,22 @@ class MPLPlot(ABC):
             #  typing.
             yield col, np.asarray(values.values)
 
-    @property
-    def nseries(self) -> int:
+    def _get_nseries(self, data: Series | DataFrame) -> int:
         # When `by` is explicitly assigned, grouped data size will be defined, and
         # this will determine number of subplots to have, aka `self.nseries`
-        if self.data.ndim == 1:
+        if data.ndim == 1:
             return 1
         elif self.by is not None and self._kind == "hist":
             return len(self._grouped)
         elif self.by is not None and self._kind == "box":
             return len(self.columns)
         else:
-            return self.data.shape[1]
+            return data.shape[1]
+
+    @final
+    @property
+    def nseries(self) -> int:
+        return self._get_nseries(self.data)
 
     @final
     def draw(self) -> None:
@@ -880,10 +891,12 @@ class MPLPlot(ABC):
         index = self.data.index
         is_datetype = index.inferred_type in ("datetime", "date", "datetime64", "time")
 
+        x: list[int] | np.ndarray
         if self.use_index:
             if convert_period and isinstance(index, ABCPeriodIndex):
                 self.data = self.data.reindex(index=index.sort_values())
-                x = self.data.index.to_timestamp()._mpl_repr()
+                index = cast("PeriodIndex", self.data.index)
+                x = index.to_timestamp()._mpl_repr()
             elif is_any_real_numeric_dtype(index.dtype):
                 # Matplotlib supports numeric values or datetime objects as
                 # xaxis values. Taking LBYL approach here, by the time
@@ -1050,8 +1063,12 @@ class MPLPlot(ABC):
             color=self.kwds.get(color_kwds),
         )
 
+    # TODO: tighter typing for first return?
     @final
-    def _parse_errorbars(self, label: str, err):
+    @staticmethod
+    def _parse_errorbars(
+        label: str, err, data: NDFrameT, nseries: int
+    ) -> tuple[Any, NDFrameT]:
         """
         Look for error keyword arguments and return the actual errorbar data
         or return the error DataFrame/dict
@@ -1071,7 +1088,7 @@ class MPLPlot(ABC):
         should be in a ``Mx2xN`` array.
         """
         if err is None:
-            return None
+            return None, data
 
         def match_labels(data, e):
             e = e.reindex(data.index)
@@ -1079,7 +1096,7 @@ class MPLPlot(ABC):
 
         # key-matched DataFrame
         if isinstance(err, ABCDataFrame):
-            err = match_labels(self.data, err)
+            err = match_labels(data, err)
         # key-matched dict
         elif isinstance(err, dict):
             pass
@@ -1087,16 +1104,16 @@ class MPLPlot(ABC):
         # Series of error values
         elif isinstance(err, ABCSeries):
             # broadcast error series across data
-            err = match_labels(self.data, err)
+            err = match_labels(data, err)
             err = np.atleast_2d(err)
-            err = np.tile(err, (self.nseries, 1))
+            err = np.tile(err, (nseries, 1))
 
         # errors are a column in the dataframe
         elif isinstance(err, str):
-            evalues = self.data[err].values
-            self.data = self.data[self.data.columns.drop(err)]
+            evalues = data[err].values
+            data = data[data.columns.drop(err)]
             err = np.atleast_2d(evalues)
-            err = np.tile(err, (self.nseries, 1))
+            err = np.tile(err, (nseries, 1))
 
         elif is_list_like(err):
             if is_iterator(err):
@@ -1108,40 +1125,40 @@ class MPLPlot(ABC):
             err_shape = err.shape
 
             # asymmetrical error bars
-            if isinstance(self.data, ABCSeries) and err_shape[0] == 2:
+            if isinstance(data, ABCSeries) and err_shape[0] == 2:
                 err = np.expand_dims(err, 0)
                 err_shape = err.shape
-                if err_shape[2] != len(self.data):
+                if err_shape[2] != len(data):
                     raise ValueError(
                         "Asymmetrical error bars should be provided "
-                        f"with the shape (2, {len(self.data)})"
+                        f"with the shape (2, {len(data)})"
                     )
-            elif isinstance(self.data, ABCDataFrame) and err.ndim == 3:
+            elif isinstance(data, ABCDataFrame) and err.ndim == 3:
                 if (
-                    (err_shape[0] != self.nseries)
+                    (err_shape[0] != nseries)
                     or (err_shape[1] != 2)
-                    or (err_shape[2] != len(self.data))
+                    or (err_shape[2] != len(data))
                 ):
                     raise ValueError(
                         "Asymmetrical error bars should be provided "
-                        f"with the shape ({self.nseries}, 2, {len(self.data)})"
+                        f"with the shape ({nseries}, 2, {len(data)})"
                     )
 
             # broadcast errors to each data series
             if len(err) == 1:
-                err = np.tile(err, (self.nseries, 1))
+                err = np.tile(err, (nseries, 1))
 
         elif is_number(err):
             err = np.tile(
                 [err],  # pyright: ignore[reportGeneralTypeIssues]
-                (self.nseries, len(self.data)),
+                (nseries, len(data)),
             )
 
         else:
             msg = f"No valid {label} detected"
             raise ValueError(msg)
 
-        return err
+        return err, data  # pyright: ignore[reportGeneralTypeIssues]
 
     @final
     def _get_errorbars(
@@ -1215,8 +1232,7 @@ class PlanePlot(MPLPlot, ABC):
         self.y = y
 
     @final
-    @property
-    def nseries(self) -> int:
+    def _get_nseries(self, data: Series | DataFrame) -> int:
         return 1
 
     @final
diff --git a/pandas/plotting/_matplotlib/hist.py b/pandas/plotting/_matplotlib/hist.py
index ed93dc740e..e42914a980 100644
--- a/pandas/plotting/_matplotlib/hist.py
+++ b/pandas/plotting/_matplotlib/hist.py
@@ -45,7 +45,10 @@ if TYPE_CHECKING:
 
     from pandas._typing import PlottingOrientation
 
-    from pandas import DataFrame
+    from pandas import (
+        DataFrame,
+        Series,
+    )
 
 
 class HistPlot(LinePlot):
@@ -87,7 +90,7 @@ class HistPlot(LinePlot):
                 bins = self._calculate_bins(self.data, bins)
         return bins
 
-    def _calculate_bins(self, data: DataFrame, bins) -> np.ndarray:
+    def _calculate_bins(self, data: Series | DataFrame, bins) -> np.ndarray:
         """Calculate bins given data"""
         nd_values = data.infer_objects(copy=False)._get_numeric_data()
         values = np.ravel(nd_values)


Test patch:
diff --git a/pandas/tests/indexes/test_index_new.py b/pandas/tests/indexes/test_index_new.py
index bfbffb4c97..72641077c9 100644
--- a/pandas/tests/indexes/test_index_new.py
+++ b/pandas/tests/indexes/test_index_new.py
@@ -4,12 +4,15 @@ Tests for the Index constructor conducting inference.
 from datetime import (
     datetime,
     timedelta,
+    timezone,
 )
 from decimal import Decimal
 
 import numpy as np
 import pytest
 
+from pandas._libs.tslibs.timezones import maybe_get_tz
+
 from pandas import (
     NA,
     Categorical,
@@ -183,6 +186,15 @@ class TestIndexConstructorInference:
         tm.assert_index_equal(Index(data), expected)
         tm.assert_index_equal(Index(np.array(data, dtype=object)), expected)
 
+    def test_constructor_datetimes_mixed_tzs(self):
+        # https://github.com/pandas-dev/pandas/pull/55793/files#r1383719998
+        tz = maybe_get_tz("US/Central")
+        dt1 = datetime(2020, 1, 1, tzinfo=tz)
+        dt2 = datetime(2020, 1, 1, tzinfo=timezone.utc)
+        result = Index([dt1, dt2])
+        expected = Index([dt1, dt2], dtype=object)
+        tm.assert_index_equal(result, expected)
+
 
 class TestDtypeEnforced:
     # check we don't silently ignore the dtype keyword
diff --git a/pandas/tests/io/excel/test_openpyxl.py b/pandas/tests/io/excel/test_openpyxl.py
index da94c74f23..53cbd1ce3c 100644
--- a/pandas/tests/io/excel/test_openpyxl.py
+++ b/pandas/tests/io/excel/test_openpyxl.py
@@ -13,6 +13,7 @@ from pandas.io.excel import (
     ExcelWriter,
     _OpenpyxlWriter,
 )
+from pandas.io.excel._openpyxl import OpenpyxlReader
 
 openpyxl = pytest.importorskip("openpyxl")
 
@@ -129,6 +130,31 @@ def test_engine_kwargs_append_data_only(ext, data_only, expected):
             # ExcelWriter needs us to writer something to close properly?
             DataFrame().to_excel(writer, sheet_name="Sheet2")
 
+        # ensure that data_only also works for reading
+        #  and that formulas/values roundtrip
+        assert (
+            pd.read_excel(
+                f,
+                sheet_name="Sheet1",
+                engine="openpyxl",
+                engine_kwargs={"data_only": data_only},
+            ).iloc[0, 1]
+            == expected
+        )
+
+
+@pytest.mark.parametrize("kwarg_name", ["read_only", "data_only"])
+@pytest.mark.parametrize("kwarg_value", [True, False])
+def test_engine_kwargs_append_reader(datapath, ext, kwarg_name, kwarg_value):
+    # GH 55027
+    # test that `read_only` and `data_only` can be passed to
+    #  `openpyxl.reader.excel.load_workbook` via `engine_kwargs`
+    filename = datapath("io", "data", "excel", "test1" + ext)
+    with contextlib.closing(
+        OpenpyxlReader(filename, engine_kwargs={kwarg_name: kwarg_value})
+    ) as reader:
+        assert getattr(reader.book, kwarg_name) == kwarg_value
+
 
 @pytest.mark.parametrize(
     "mode,expected", [("w", ["baz"]), ("a", ["foo", "bar", "baz"])]
diff --git a/pandas/tests/tools/test_to_datetime.py b/pandas/tests/tools/test_to_datetime.py
index 0ea8da89f2..3d3ca99f4d 100644
--- a/pandas/tests/tools/test_to_datetime.py
+++ b/pandas/tests/tools/test_to_datetime.py
@@ -3727,3 +3727,141 @@ def test_to_datetime_with_empty_str_utc_false_offsets_and_format_mixed():
         to_datetime(
             ["2020-01-01 00:00+00:00", "2020-01-01 00:00+02:00", ""], format="mixed"
         )
+
+
+def test_to_datetime_mixed_tzs_mixed_types():
+    # GH#55793, GH#55693 mismatched tzs but one is str and other is
+    #  datetime object
+    ts = Timestamp("2016-01-02 03:04:05", tz="US/Pacific")
+    dtstr = "2023-10-30 15:06+01"
+    arr = [ts, dtstr]
+
+    msg = (
+        "Mixed timezones detected. pass utc=True in to_datetime or tz='UTC' "
+        "in DatetimeIndex to convert to a common timezone"
+    )
+    with pytest.raises(ValueError, match=msg):
+        to_datetime(arr)
+    with pytest.raises(ValueError, match=msg):
+        to_datetime(arr, format="mixed")
+    with pytest.raises(ValueError, match=msg):
+        DatetimeIndex(arr)
+
+
+def test_to_datetime_mixed_types_matching_tzs():
+    # GH#55793
+    dtstr = "2023-11-01 09:22:03-07:00"
+    ts = Timestamp(dtstr)
+    arr = [ts, dtstr]
+    res1 = to_datetime(arr)
+    res2 = to_datetime(arr[::-1])[::-1]
+    res3 = to_datetime(arr, format="mixed")
+    res4 = DatetimeIndex(arr)
+
+    expected = DatetimeIndex([ts, ts])
+    tm.assert_index_equal(res1, expected)
+    tm.assert_index_equal(res2, expected)
+    tm.assert_index_equal(res3, expected)
+    tm.assert_index_equal(res4, expected)
+
+
+dtstr = "2020-01-01 00:00+00:00"
+ts = Timestamp(dtstr)
+
+
+@pytest.mark.filterwarnings("ignore:Could not infer format:UserWarning")
+@pytest.mark.parametrize(
+    "aware_val",
+    [dtstr, Timestamp(dtstr)],
+    ids=lambda x: type(x).__name__,
+)
+@pytest.mark.parametrize(
+    "naive_val",
+    [dtstr[:-6], ts.tz_localize(None), ts.date(), ts.asm8, ts.value, float(ts.value)],
+    ids=lambda x: type(x).__name__,
+)
+@pytest.mark.parametrize("naive_first", [True, False])
+def test_to_datetime_mixed_awareness_mixed_types(aware_val, naive_val, naive_first):
+    # GH#55793, GH#55693
+    # Empty string parses to NaT
+    vals = [aware_val, naive_val, ""]
+
+    vec = vals
+    if naive_first:
+        # alas, the behavior is order-dependent, so we test both ways
+        vec = [naive_val, aware_val, ""]
+
+    # both_strs-> paths that were previously already deprecated with warning
+    #  issued in _array_to_datetime_object
+    both_strs = isinstance(aware_val, str) and isinstance(naive_val, str)
+    has_numeric = isinstance(naive_val, (int, float))
+
+    depr_msg = "In a future version of pandas, parsing datetimes with mixed time zones"
+
+    first_non_null = next(x for x in vec if x != "")
+    # if first_non_null is a not a string, _guess_datetime_format_for_array
+    #  doesn't guess a format so we don't go through array_strptime
+    if not isinstance(first_non_null, str):
+        # that case goes through array_strptime which has different behavior
+        msg = "Cannot mix tz-aware with tz-naive values"
+        if naive_first and isinstance(aware_val, Timestamp):
+            if isinstance(naive_val, Timestamp):
+                msg = "Tz-aware datetime.datetime cannot be converted to datetime64"
+            with pytest.raises(ValueError, match=msg):
+                to_datetime(vec)
+        else:
+            with pytest.raises(ValueError, match=msg):
+                to_datetime(vec)
+
+        # No warning/error with utc=True
+        to_datetime(vec, utc=True)
+
+    elif has_numeric and vec.index(aware_val) < vec.index(naive_val):
+        msg = "time data .* doesn't match format"
+        with pytest.raises(ValueError, match=msg):
+            to_datetime(vec)
+        with pytest.raises(ValueError, match=msg):
+            to_datetime(vec, utc=True)
+
+    elif both_strs and vec.index(aware_val) < vec.index(naive_val):
+        msg = r"time data \"2020-01-01 00:00\" doesn't match format"
+        with pytest.raises(ValueError, match=msg):
+            to_datetime(vec)
+        with pytest.raises(ValueError, match=msg):
+            to_datetime(vec, utc=True)
+
+    elif both_strs and vec.index(naive_val) < vec.index(aware_val):
+        msg = "unconverted data remains when parsing with format"
+        with pytest.raises(ValueError, match=msg):
+            to_datetime(vec)
+        with pytest.raises(ValueError, match=msg):
+            to_datetime(vec, utc=True)
+
+    else:
+        with tm.assert_produces_warning(FutureWarning, match=depr_msg):
+            to_datetime(vec)
+
+        # No warning/error with utc=True
+        to_datetime(vec, utc=True)
+
+    if both_strs:
+        with tm.assert_produces_warning(FutureWarning, match=depr_msg):
+            to_datetime(vec, format="mixed")
+        with tm.assert_produces_warning(FutureWarning, match=depr_msg):
+            msg = "DatetimeIndex has mixed timezones"
+            with pytest.raises(TypeError, match=msg):
+                DatetimeIndex(vec)
+    else:
+        msg = "Cannot mix tz-aware with tz-naive values"
+        if naive_first and isinstance(aware_val, Timestamp):
+            if isinstance(naive_val, Timestamp):
+                msg = "Tz-aware datetime.datetime cannot be converted to datetime64"
+            with pytest.raises(ValueError, match=msg):
+                to_datetime(vec, format="mixed")
+            with pytest.raises(ValueError, match=msg):
+                DatetimeIndex(vec)
+        else:
+            with pytest.raises(ValueError, match=msg):
+                to_datetime(vec, format="mixed")
+            with pytest.raises(ValueError, match=msg):
+                DatetimeIndex(vec)