Repository: pandas-dev/pandas (python)

Issue:
BUG: StataWriter returns ascii error when length of string is < 2045, but encoded length is > 2045
### Pandas version checks

- [x] I have checked that this issue has not already been reported.

- [x] I have confirmed this bug exists on the [latest version](https://pandas.pydata.org/docs/whatsnew/index.html) of pandas.

- [x] I have confirmed this bug exists on the [main branch](https://pandas.pydata.org/docs/dev/getting_started/install.html#installing-the-development-version-of-pandas) of pandas.


### Reproducible Example

```python
import pandas as pd

df = pd.DataFrame({'doubleByteCol': ['§'*1500]})
df.to_stata('temp.dta', version=118)

len_encoded = df['doubleByteCol'].str.encode('utf-8').str.len()     # _encode_strings() count = 3000 -> no byte encoding because assumed will become strL (stata.py:2694)
len_typlist = df['doubleByteCol'].str.len()                         # _dtype_to_stata_type() = 1500 -> typ 1500 (stata.py:2193)
len_typlist < 2045      # True -> Tries to convert to np dtype S1500, but fails because unicode characters are not supported (normally no issue because encoded to bytes first) (stata.py:2945,2956)
```

### Issue Description

The StataWriter uses two different versions of the string column to check the same thing. During _encode_strings() it checks the length of the byte-encoded column `max_len_string_array(ensure_object(encoded._values))` but when assigning numpy types it checks the (potentially) unencoded version `itemsize = max_len_string_array(ensure_object(column._values))`. This then trips up the _prepare_data() section, which expects short columns to be byte-encoded already `typ <= self._max_string_length` based on the reported type, which is not true if the encoded column > 2045 due to unicode characters such as `§` taking up two bytes.

### Expected Behavior

I don't know the internal workings of stata.py well enough to be sure, but I think the easiest fix is using the actual values when checking str length in _encode_strings(). That is, replace
```max_len_string_array(ensure_object(encoded._values))```
by
```max_len_string_array(ensure_object(self.data[col]._values))```

### Installed Versions

<details>

INSTALLED VERSIONS
------------------
commit                : d9cdd2ee5a58015ef6f4d15c7226110c9aab8140
python                : 3.12.1.final.0
python-bits           : 64
OS                    : Windows
OS-release            : 11
Version               : 10.0.26100
machine               : AMD64
processor             : AMD64 Family 25 Model 33 Stepping 0, AuthenticAMD
byteorder             : little
LC_ALL                : None
LANG                  : en_US.UTF-8
LOCALE                : English_Belgium.1252

pandas                : 2.2.2
numpy                 : 1.26.4
pytz                  : 2024.1
dateutil              : 2.9.0.post0
setuptools            : None
pip                   : 23.2.1
Cython                : None
pytest                : 8.3.5
hypothesis            : None
sphinx                : None
blosc                 : None
feather               : None
xlsxwriter            : None
lxml.etree            : 4.9.4
html5lib              : None
pymysql               : None
psycopg2              : None
jinja2                : None
IPython               : None
pandas_datareader     : None
adbc-driver-postgresql: None
adbc-driver-sqlite    : None
bs4                   : None
bottleneck            : None
dataframe-api-compat  : None
fastparquet           : None
fsspec                : None
gcsfs                 : None
matplotlib            : None
numba                 : None
numexpr               : None
odfpy                 : None
openpyxl              : None
pandas_gbq            : None
pyarrow               : 15.0.2
pyreadstat            : None
python-calamine       : None
pyxlsb                : None
s3fs                  : None
scipy                 : None
sqlalchemy            : None
tables                : None
tabulate              : None
xarray                : None
xlrd                  : None
zstandard             : None
tzdata                : 2024.1
qtpy                  : None
pyqt5                 : None

</details>

### Temporary fix
For users finding this topic, this refers to the following Exception
```
Exception has occurred: UnicodeEncodeError       (note: full exception trace is shown but execution is paused at: <module>)
'ascii' codec can't encode characters in position 0-1499: ordinal not in range(128)
  File "F:\datatog\junkyard\adhoc-scripts\mwes\pandas_asciiencoding.py", line 4, in <module> (Current frame)
    df.to_stata('temp.dta', version=118)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1499: ordinal not in range(128)
```

You can workaround this issue by explicitly specifying the offending columns in the `convert_strL` option.

Pull request:
BUG: to_stata erroring when encoded text and normal text have mismatched length
- [x] closes #61583  (Replace xxxx with the GitHub issue number)
- [x] [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
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [x] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.

I removed the encoded check in stata.py and replaced it with a normal check, if the encoded check was there for any particular reason I can standardize them the other way

Patch:
diff --git a/AUTHORS.md b/AUTHORS.md
index 84fcfe05e3..e8885baa98 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -7,12 +7,12 @@ About the Copyright Holders
     led by Wes McKinney. AQR released the source under this license in 2009.
 *   Copyright (c) 2011-2012, Lambda Foundry, Inc.
 
-    Wes is now an employee of Lambda Foundry, and remains the pandas project
+    Wes became an employee of Lambda Foundry, and remained the pandas project
     lead.
 *   Copyright (c) 2011-2012, PyData Development Team
 
     The PyData Development Team is the collection of developers of the PyData
-    project. This includes all of the PyData sub-projects, including pandas. The
+    project. This includes all of the PyData sub-projects, such as pandas. The
     core team that coordinates development on GitHub can be found here:
     https://github.com/pydata.
 
@@ -23,11 +23,11 @@ Our Copyright Policy
 
 PyData uses a shared copyright model. Each contributor maintains copyright
 over their contributions to PyData. However, it is important to note that
-these contributions are typically only changes to the repositories. Thus,
+these contributions are typically limited to changes to the repositories. Thus,
 the PyData source code, in its entirety, is not the copyright of any single
 person or institution. Instead, it is the collective copyright of the
 entire PyData Development Team. If individual contributors want to maintain
-a record of what changes/contributions they have specific copyright on,
+a record of the specific changes or contributions they hold copyright to,
 they should indicate their copyright in the commit message of the change
 when they commit the change to one of the PyData repositories.
 
@@ -50,7 +50,7 @@ Other licenses can be found in the LICENSES directory.
 License
 =======
 
-pandas is distributed under a 3-clause ("Simplified" or "New") BSD
+pandas is distributed under the 3-clause ("Simplified" or "New") BSD
 license. Parts of NumPy, SciPy, numpydoc, bottleneck, which all have
-BSD-compatible licenses, are included. Their licenses follow the pandas
+BSD-compatible licenses, are included. Their licenses are compatible with the pandas
 license.
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 677ee6274b..f222a22853 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -114,6 +114,8 @@ if pattern:
                 ):
                     exclude_patterns.append(rel_fname)
                 elif single_doc and rel_fname != pattern:
+                    if "\\" in rel_fname:
+                        rel_fname = rel_fname.replace("\\", "/")
                     exclude_patterns.append(rel_fname)
 
 with open(os.path.join(source_path, "index.rst.template"), encoding="utf-8") as f:
diff --git a/doc/source/development/community.rst b/doc/source/development/community.rst
index 1c698d130e..e139ea0376 100644
--- a/doc/source/development/community.rst
+++ b/doc/source/development/community.rst
@@ -114,7 +114,7 @@ people who are hesitant to bring up their questions or ideas on a large public
 mailing list or GitHub.
 
 If this sounds like the right place for you, you are welcome to join using
-`this link <https://join.slack.com/t/pandas-dev-community/shared_invite/zt-2blg6u9k3-K6_XvMRDZWeH7Id274UeIg>`_!
+`this link <https://join.slack.com/t/pandas-dev-community/shared_invite/zt-3813u5fme-hmp5izpbeFl9G8~smrkE~A>`_!
 Please remember to follow our `Code of Conduct <https://pandas.pydata.org/community/coc.html>`_,
 and be aware that our admins are monitoring for irrelevant messages and will remove folks who use
 our
diff --git a/doc/source/reference/indexing.rst b/doc/source/reference/indexing.rst
index 7a4bc0f467..79a49b2030 100644
--- a/doc/source/reference/indexing.rst
+++ b/doc/source/reference/indexing.rst
@@ -98,6 +98,7 @@ Conversion
    :toctree: api/
 
    Index.astype
+   Index.infer_objects
    Index.item
    Index.map
    Index.ravel
diff --git a/doc/source/user_guide/enhancingperf.rst b/doc/source/user_guide/enhancingperf.rst
index 647b0f760f..9c37f317a8 100644
--- a/doc/source/user_guide/enhancingperf.rst
+++ b/doc/source/user_guide/enhancingperf.rst
@@ -50,7 +50,7 @@ We have a :class:`DataFrame` to which we want to apply a function row-wise.
        {
            "a": np.random.randn(1000),
            "b": np.random.randn(1000),
-           "N": np.random.randint(100, 1000, (1000)),
+           "N": np.random.randint(100, 1000, (1000), dtype="int64"),
            "x": "x",
        }
    )
@@ -83,7 +83,7 @@ using the `prun ipython magic function <https://ipython.readthedocs.io/en/stable
 .. ipython:: python
 
    # most time consuming 4 calls
-   %prun -l 4 df.apply(lambda x: integrate_f(x["a"], x["b"], x["N"]), axis=1)  # noqa E999
+   %prun -l 4 df.apply(lambda x: integrate_f(x['a'], x['b'], x['N']), axis=1)
 
 By far the majority of time is spend inside either ``integrate_f`` or ``f``,
 hence we'll concentrate our efforts cythonizing these two functions.
@@ -164,7 +164,7 @@ can be improved by passing an ``np.ndarray``.
 
 .. ipython:: python
 
-   %prun -l 4 df.apply(lambda x: integrate_f_typed(x["a"], x["b"], x["N"]), axis=1)
+   %prun -l 4 df.apply(lambda x: integrate_f_typed(x['a'], x['b'], x['N']), axis=1)
 
 .. ipython::
 
@@ -204,7 +204,7 @@ calls are needed to utilize this function.
 
 .. ipython:: python
 
-   %timeit apply_integrate_f(df["a"].to_numpy(), df["b"].to_numpy(), df["N"].to_numpy())
+   %timeit apply_integrate_f(df['a'].to_numpy(), df['b'].to_numpy(), df['N'].to_numpy())
 
 Performance has improved from the prior implementation by almost ten times.
 
@@ -218,7 +218,7 @@ and ``wraparound`` checks can yield more performance.
 
 .. ipython:: python
 
-   %prun -l 4 apply_integrate_f(df["a"].to_numpy(), df["b"].to_numpy(), df["N"].to_numpy())
+   %prun -l 4 apply_integrate_f(df['a'].to_numpy(), df['b'].to_numpy(), df['N'].to_numpy())
 
 .. ipython::
 
@@ -253,7 +253,7 @@ and ``wraparound`` checks can yield more performance.
 
 .. ipython:: python
 
-   %timeit apply_integrate_f_wrap(df["a"].to_numpy(), df["b"].to_numpy(), df["N"].to_numpy())
+   %timeit apply_integrate_f_wrap(df['a'].to_numpy(), df['b'].to_numpy(), df['N'].to_numpy())
 
 However, a loop indexer ``i`` accessing an invalid location in an array would cause a segfault because memory access isn't checked.
 For more about ``boundscheck`` and ``wraparound``, see the Cython docs on
diff --git a/doc/source/whatsnew/index.rst b/doc/source/whatsnew/index.rst
index 1dd6c5fabe..9da73c8fd7 100644
--- a/doc/source/whatsnew/index.rst
+++ b/doc/source/whatsnew/index.rst
@@ -24,6 +24,7 @@ Version 2.3
 .. toctree::
    :maxdepth: 2
 
+   v2.3.1
    v2.3.0
 
 Version 2.2
diff --git a/doc/source/whatsnew/v2.3.0.rst b/doc/source/whatsnew/v2.3.0.rst
index 6433fe8d2b..8ca6c0006a 100644
--- a/doc/source/whatsnew/v2.3.0.rst
+++ b/doc/source/whatsnew/v2.3.0.rst
@@ -31,39 +31,6 @@ Other enhancements
 - The :meth:`~Series.cumsum`, :meth:`~Series.cummin`, and :meth:`~Series.cummax` reductions are now implemented for :class:`StringDtype` columns (:issue:`60633`)
 - The :meth:`~Series.sum` reduction is now implemented for :class:`StringDtype` columns (:issue:`59853`)
 
-.. ---------------------------------------------------------------------------
-.. _whatsnew_230.notable_bug_fixes:
-
-Notable bug fixes
-~~~~~~~~~~~~~~~~~
-
-These are bug fixes that might have notable behavior changes.
-
-.. _whatsnew_230.notable_bug_fixes.string_comparisons:
-
-Comparisons between different string dtypes
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-In previous versions, comparing :class:`Series` of different string dtypes (e.g. ``pd.StringDtype("pyarrow", na_value=pd.NA)`` against ``pd.StringDtype("python", na_value=np.nan)``) would result in inconsistent resulting dtype or incorrectly raise. pandas will now use the hierarchy
-
-    object < (python, NaN) < (pyarrow, NaN) < (python, NA) < (pyarrow, NA)
-
-in determining the result dtype when there are different string dtypes compared. Some examples:
-
-- When ``pd.StringDtype("pyarrow", na_value=pd.NA)`` is compared against any other string dtype, the result will always be ``boolean[pyarrow]``.
-- When ``pd.StringDtype("python", na_value=pd.NA)`` is compared against ``pd.StringDtype("pyarrow", na_value=np.nan)``, the result will be ``boolean``, the NumPy-backed nullable extension array.
-- When ``pd.StringDtype("python", na_value=pd.NA)`` is compared against ``pd.StringDtype("python", na_value=np.nan)``, the result will be ``boolean``, the NumPy-backed nullable extension array.
-
-.. _whatsnew_230.api_changes:
-
-API changes
-~~~~~~~~~~~
-
-- When enabling the ``future.infer_string`` option, :class:`Index` set operations (like
-  union or intersection) will now ignore the dtype of an empty :class:`RangeIndex` or
-  empty :class:`Index` with ``object`` dtype when determining the dtype of the resulting
-  Index (:issue:`60797`)
-
 .. ---------------------------------------------------------------------------
 .. _whatsnew_230.deprecations:
 
@@ -85,8 +52,6 @@ Numeric
 
 Strings
 ^^^^^^^
-- Bug in :meth:`.DataFrameGroupBy.min`, :meth:`.DataFrameGroupBy.max`, :meth:`.Resampler.min`, :meth:`.Resampler.max` where all NA values of string dtype would return float instead of string dtype (:issue:`60810`)
-- Bug in :meth:`DataFrame.sum` with ``axis=1``, :meth:`.DataFrameGroupBy.sum` or :meth:`.SeriesGroupBy.sum` with ``skipna=True``, and :meth:`.Resampler.sum` with all NA values of :class:`StringDtype` resulted in ``0`` instead of the empty string ``""`` (:issue:`60229`)
 - Bug in :meth:`Series.__pos__` and :meth:`DataFrame.__pos__` where an ``Exception`` was not raised for :class:`StringDtype` with ``storage="pyarrow"`` (:issue:`60710`)
 - Bug in :meth:`Series.rank` for :class:`StringDtype` with ``storage="pyarrow"`` that incorrectly returned integer results with ``method="average"`` and raised an error if it would truncate results (:issue:`59768`)
 - Bug in :meth:`Series.replace` with :class:`StringDtype` when replacing with a non-string value was not upcasting to ``object`` dtype (:issue:`60282`)
diff --git a/doc/source/whatsnew/v2.3.1.rst b/doc/source/whatsnew/v2.3.1.rst
new file mode 100644
index 0000000000..64e5c1510e
--- /dev/null
+++ b/doc/source/whatsnew/v2.3.1.rst
@@ -0,0 +1,88 @@
+.. _whatsnew_231:
+
+What's new in 2.3.1 (Month XX, 2025)
+------------------------------------
+
+These are the changes in pandas 2.3.1. See :ref:`release` for a full changelog
+including other versions of pandas.
+
+{{ header }}
+
+.. ---------------------------------------------------------------------------
+.. _whatsnew_231.string_fixes:
+
+Improvements and fixes for the StringDtype
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. _whatsnew_231.string_fixes.string_comparisons:
+
+Comparisons between different string dtypes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In previous versions, comparing :class:`Series` of different string dtypes (e.g. ``pd.StringDtype("pyarrow", na_value=pd.NA)`` against ``pd.StringDtype("python", na_value=np.nan)``) would result in inconsistent resulting dtype or incorrectly raise. pandas will now use the hierarchy
+
+    object < (python, NaN) < (pyarrow, NaN) < (python, NA) < (pyarrow, NA)
+
+in determining the result dtype when there are different string dtypes compared. Some examples:
+
+- When ``pd.StringDtype("pyarrow", na_value=pd.NA)`` is compared against any other string dtype, the result will always be ``boolean[pyarrow]``.
+- When ``pd.StringDtype("python", na_value=pd.NA)`` is compared against ``pd.StringDtype("pyarrow", na_value=np.nan)``, the result will be ``boolean``, the NumPy-backed nullable extension array.
+- When ``pd.StringDtype("python", na_value=pd.NA)`` is compared against ``pd.StringDtype("python", na_value=np.nan)``, the result will be ``boolean``, the NumPy-backed nullable extension array.
+
+.. _whatsnew_231.string_fixes.ignore_empty:
+
+Index set operations ignore empty RangeIndex and object dtype Index
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When enabling the ``future.infer_string`` option, :class:`Index` set operations (like
+union or intersection) will now ignore the dtype of an empty :class:`RangeIndex` or
+empty :class:`Index` with ``object`` dtype when determining the dtype of the resulting
+Index (:issue:`60797`).
+
+This ensures that combining such empty Index with strings will infer the string dtype
+correctly, rather than defaulting to ``object`` dtype. For example:
+
+.. code-block:: python
+
+    >>> pd.options.mode.infer_string = True
+    >>> df = pd.DataFrame()
+    >>> df.columns.dtype
+    dtype('int64')               # default RangeIndex for empty columns
+    >>> df["a"] = [1, 2, 3]
+    >>> df.columns.dtype
+    <StringDtype(na_value=nan)>  # new columns use string dtype instead of object dtype
+
+.. _whatsnew_231.string_fixes.bugs:
+
+Bug fixes
+^^^^^^^^^
+- Bug in :meth:`.DataFrameGroupBy.min`, :meth:`.DataFrameGroupBy.max`, :meth:`.Resampler.min`, :meth:`.Resampler.max` where all NA values of string dtype would return float instead of string dtype (:issue:`60810`)
+- Bug in :meth:`DataFrame.sum` with ``axis=1``, :meth:`.DataFrameGroupBy.sum` or :meth:`.SeriesGroupBy.sum` with ``skipna=True``, and :meth:`.Resampler.sum` with all NA values of :class:`StringDtype` resulted in ``0`` instead of the empty string ``""`` (:issue:`60229`)
+- Fixed bug in :meth:`DataFrame.explode` and :meth:`Series.explode` where methods would fail with ``dtype="str"`` (:issue:`61623`)
+
+
+.. _whatsnew_231.regressions:
+
+Fixed regressions
+~~~~~~~~~~~~~~~~~
+-
+
+.. ---------------------------------------------------------------------------
+.. _whatsnew_231.bug_fixes:
+
+Bug fixes
+~~~~~~~~~
+-
+
+.. ---------------------------------------------------------------------------
+.. _whatsnew_231.other:
+
+Other
+~~~~~
+-
+
+.. ---------------------------------------------------------------------------
+.. _whatsnew_231.contributors:
+
+Contributors
+~~~~~~~~~~~~
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 8d3ac0e396..10bf1c0885 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -777,6 +777,7 @@ I/O
 - Bug in :meth:`DataFrame.to_excel` when writing empty :class:`DataFrame` with :class:`MultiIndex` on both axes (:issue:`57696`)
 - Bug in :meth:`DataFrame.to_excel` where the :class:`MultiIndex` index with a period level was not a date (:issue:`60099`)
 - Bug in :meth:`DataFrame.to_stata` when exporting a column containing both long strings (Stata strL) and :class:`pd.NA` values (:issue:`23633`)
+- Bug in :meth:`DataFrame.to_stata` when input encoded length and normal length are mismatched (:issue:`61583`)
 - Bug in :meth:`DataFrame.to_stata` when writing :class:`DataFrame` and ``byteorder=`big```. (:issue:`58969`)
 - Bug in :meth:`DataFrame.to_stata` when writing more than 32,000 value labels. (:issue:`60107`)
 - Bug in :meth:`DataFrame.to_string` that raised ``StopIteration`` with nested DataFrames. (:issue:`16098`)
diff --git a/pandas/_libs/src/datetime/pd_datetime.c b/pandas/_libs/src/datetime/pd_datetime.c
index 2c32fb0481..bf1a0c82e7 100644
--- a/pandas/_libs/src/datetime/pd_datetime.c
+++ b/pandas/_libs/src/datetime/pd_datetime.c
@@ -192,6 +192,10 @@ static npy_datetime PyDateTimeToEpoch(PyObject *dt, NPY_DATETIMEUNIT base) {
   return npy_dt;
 }
 
+/* Initializes and exposes a customer datetime C-API from the pandas library
+ * by creating a PyCapsule that stores function pointers, which can be accessed
+ * later by other C code or Cython code that imports the capsule.
+ */
 static int pandas_datetime_exec(PyObject *Py_UNUSED(module)) {
   PyDateTime_IMPORT;
   PandasDateTime_CAPI *capi = PyMem_Malloc(sizeof(PandasDateTime_CAPI));
diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py
index 0b90bcea35..c18f06c3a1 100644
--- a/pandas/core/arrays/arrow/array.py
+++ b/pandas/core/arrays/arrow/array.py
@@ -1935,9 +1935,9 @@ class ArrowExtensionArray(
         """
         # child class explode method supports only list types; return
         # default implementation for non list types.
-        if not (
-            pa.types.is_list(self.dtype.pyarrow_dtype)
-            or pa.types.is_large_list(self.dtype.pyarrow_dtype)
+        if not hasattr(self.dtype, "pyarrow_dtype") or (
+            not pa.types.is_list(self.dtype.pyarrow_dtype)
+            and not pa.types.is_large_list(self.dtype.pyarrow_dtype)
         ):
             return super()._explode()
         values = self
diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py
index 5fde6577e9..f2cf1a4838 100644
--- a/pandas/io/formats/excel.py
+++ b/pandas/io/formats/excel.py
@@ -303,13 +303,13 @@ class CSSToExcelConverter:
         #       'slantDashDot'
         #       'thick'
         #       'thin'
-        if width is None and style is None and color is None:
-            # Return None will remove "border" from style dictionary
-            return None
-
         if width is None and style is None:
-            # Return "none" will keep "border" in style dictionary
-            return "none"
+            if color is None:
+                # Return None will remove "border" from style dictionary
+                return None
+            else:
+                # Return "none" will keep "border" in style dictionary
+                return "none"
 
         if style in ("none", "hidden"):
             return "none"
@@ -411,11 +411,6 @@ class CSSToExcelConverter:
         else:
             return ()
 
-    def _get_underline(self, decoration: Sequence[str]) -> str | None:
-        if "underline" in decoration:
-            return "single"
-        return None
-
     def _get_shadow(self, props: Mapping[str, str]) -> bool | None:
         if "text-shadow" in props:
             return bool(re.search("^[^#(]*[1-9]", props["text-shadow"]))
diff --git a/pandas/io/stata.py b/pandas/io/stata.py
index cd290710dd..092c24f0d3 100644
--- a/pandas/io/stata.py
+++ b/pandas/io/stata.py
@@ -2739,7 +2739,7 @@ supported types."""
                 encoded = self.data[col].str.encode(self._encoding)
                 # If larger than _max_string_length do nothing
                 if (
-                    max_len_string_array(ensure_object(encoded._values))
+                    max_len_string_array(ensure_object(self.data[col]._values))
                     <= self._max_string_length
                 ):
                     self.data[col] = encoded
@@ -3263,11 +3263,15 @@ class StataStrLWriter:
             bio.write(gso_type)
 
             # llll
-            utf8_string = bytes(strl, "utf-8")
-            bio.write(struct.pack(len_type, len(utf8_string) + 1))
+            if isinstance(strl, str):
+                strl_convert = bytes(strl, "utf-8")
+            else:
+                strl_convert = strl
+
+            bio.write(struct.pack(len_type, len(strl_convert) + 1))
 
             # xxx...xxx
-            bio.write(utf8_string)
+            bio.write(strl_convert)
             bio.write(null)
 
         return bio.getvalue()
diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py
index 944575dcc8..3c222c7701 100755
--- a/scripts/validate_docstrings.py
+++ b/scripts/validate_docstrings.py
@@ -69,8 +69,10 @@ ERROR_MSGS = {
 }
 ALL_ERRORS = set(NUMPYDOC_ERROR_MSGS).union(set(ERROR_MSGS))
 duplicated_errors = set(NUMPYDOC_ERROR_MSGS).intersection(set(ERROR_MSGS))
-assert not duplicated_errors, (f"Errors {duplicated_errors} exist in both pandas "
-                               "and numpydoc, should they be removed from pandas?")
+assert not duplicated_errors, (
+    f"Errors {duplicated_errors} exist in both pandas "
+    "and numpydoc, should they be removed from pandas?"
+)
 
 
 def pandas_error(code, **kwargs):
@@ -245,7 +247,15 @@ def pandas_validate(func_name: str):
     # Some objects are instances, e.g. IndexSlice, which numpydoc can't validate
     doc_obj = get_doc_object(func_obj, doc=func_obj.__doc__)
     doc = PandasDocstring(func_name, doc_obj)
-    result = validate(doc_obj)
+    if func_obj.__doc__ is not None:
+        result = validate(doc_obj)
+    else:
+        result = {
+            "docstring": "",
+            "file": None,
+            "file_line": None,
+            "errors": [("GL08", "The object does not have a docstring")],
+        }
     mentioned_errs = doc.mentioned_private_classes
     if mentioned_errs:
         result["errors"].append(
@@ -257,7 +267,7 @@ def pandas_validate(func_name: str):
             pandas_error(
                 "SA05",
                 reference_name=rel_name,
-                right_reference=rel_name[len("pandas."):],
+                right_reference=rel_name[len("pandas.") :],
             )
             for rel_name in doc.see_also
             if rel_name.startswith("pandas.")
@@ -365,12 +375,13 @@ def print_validate_all_results(
     for func_name, res in result.items():
         error_messages = dict(res["errors"])
         actual_failures = set(error_messages)
-        expected_failures = (ignore_errors.get(func_name, set())
-                             | ignore_errors.get(None, set()))
+        expected_failures = ignore_errors.get(func_name, set()) | ignore_errors.get(
+            None, set()
+        )
         for err_code in actual_failures - expected_failures:
             sys.stdout.write(
                 f'{prefix}{res["file"]}:{res["file_line"]}:'
-                f'{err_code}:{func_name}:{error_messages[err_code]}\n'
+                f"{err_code}:{func_name}:{error_messages[err_code]}\n"
             )
             exit_status += 1
         for err_code in ignore_errors.get(func_name, set()) - actual_failures:
@@ -384,8 +395,9 @@ def print_validate_all_results(
     return exit_status
 
 
-def print_validate_one_results(func_name: str,
-                               ignore_errors: dict[str, set[str]]) -> int:
+def print_validate_one_results(
+    func_name: str, ignore_errors: dict[str, set[str]]
+) -> int:
     def header(title, width=80, char="#") -> str:
         full_line = char * width
         side_len = (width - len(title) - 2) // 2
@@ -396,8 +408,11 @@ def print_validate_one_results(func_name: str,
 
     result = pandas_validate(func_name)
 
-    result["errors"] = [(code, message) for code, message in result["errors"]
-                        if code not in ignore_errors.get(None, set())]
+    result["errors"] = [
+        (code, message)
+        for code, message in result["errors"]
+        if code not in ignore_errors.get(None, set())
+    ]
 
     sys.stderr.write(header(f"Docstring ({func_name})"))
     sys.stderr.write(f"{result['docstring']}\n")
@@ -431,14 +446,16 @@ def _format_ignore_errors(raw_ignore_errors):
                     raise ValueError(
                         f"Object `{obj_name}` is present in more than one "
                         "--ignore_errors argument. Please use it once and specify "
-                        "the errors separated by commas.")
+                        "the errors separated by commas."
+                    )
                 ignore_errors[obj_name] = set(error_codes.split(","))
 
                 unknown_errors = ignore_errors[obj_name] - ALL_ERRORS
                 if unknown_errors:
                     raise ValueError(
                         f"Object `{obj_name}` is ignoring errors {unknown_errors} "
-                        f"which are not known. Known errors are: {ALL_ERRORS}")
+                        f"which are not known. Known errors are: {ALL_ERRORS}"
+                    )
 
             # global errors "PR02,ES01"
             else:
@@ -448,27 +465,19 @@ def _format_ignore_errors(raw_ignore_errors):
         if unknown_errors:
             raise ValueError(
                 f"Unknown errors {unknown_errors} specified using --ignore_errors "
-                "Known errors are: {ALL_ERRORS}")
+                "Known errors are: {ALL_ERRORS}"
+            )
 
     return ignore_errors
 
 
-def main(
-    func_name,
-    output_format,
-    prefix,
-    ignore_deprecated,
-    ignore_errors
-):
+def main(func_name, output_format, prefix, ignore_deprecated, ignore_errors):
     """
     Main entry point. Call the validation for one or for all docstrings.
     """
     if func_name is None:
         return print_validate_all_results(
-            output_format,
-            prefix,
-            ignore_deprecated,
-            ignore_errors
+            output_format, prefix, ignore_deprecated, ignore_errors
         )
     else:
         return print_validate_one_results(func_name, ignore_errors)
@@ -524,10 +533,11 @@ if __name__ == "__main__":
     args = argparser.parse_args(sys.argv[1:])
 
     sys.exit(
-        main(args.function,
-             args.format,
-             args.prefix,
-             args.ignore_deprecated,
-             _format_ignore_errors(args.ignore_errors),
-             )
+        main(
+            args.function,
+            args.format,
+            args.prefix,
+            args.ignore_deprecated,
+            _format_ignore_errors(args.ignore_errors),
+        )
     )


Test patch:
diff --git a/pandas/tests/frame/methods/test_explode.py b/pandas/tests/frame/methods/test_explode.py
index 876ad5539d..cfb85d261d 100644
--- a/pandas/tests/frame/methods/test_explode.py
+++ b/pandas/tests/frame/methods/test_explode.py
@@ -297,3 +297,11 @@ def test_multi_columns_nan_empty():
         index=[0, 0, 1, 2, 3, 3],
     )
     tm.assert_frame_equal(result, expected)
+
+
+def test_str_dtype():
+    # https://github.com/pandas-dev/pandas/pull/61623
+    df = pd.DataFrame({"a": ["x", "y"]}, dtype="str")
+    result = df.explode(column="a")
+    assert result is not df
+    tm.assert_frame_equal(result, df)
diff --git a/pandas/tests/io/formats/test_to_excel.py b/pandas/tests/io/formats/test_to_excel.py
index b40201b9ba..babbfcb46a 100644
--- a/pandas/tests/io/formats/test_to_excel.py
+++ b/pandas/tests/io/formats/test_to_excel.py
@@ -156,6 +156,14 @@ from pandas.io.formats.excel import (
             "border-top-style: solid; border-top-width: 4pt",
             {"border": {"top": {"style": "thick"}}},
         ),
+        (
+            "border-top-style: solid; border-top-width: none",
+            {"border": {"top": {"style": "none"}}},
+        ),
+        (
+            "border-top-style: solid; border-top-width: 0.000001pt",
+            {"border": {"top": {"style": "none"}}},
+        ),
         (
             "border-top-style: dotted",
             {"border": {"top": {"style": "mediumDashDotDot"}}},
@@ -194,6 +202,10 @@ from pandas.io.formats.excel import (
             "border-top-color: blue",
             {"border": {"top": {"color": "0000FF", "style": "none"}}},
         ),
+        (
+            "border-top-style: slantDashDot; border-top-color: blue",
+            {"border": {"top": {"style": "slantDashDot", "color": "0000FF"}}},
+        ),
         # ALIGNMENT
         # - horizontal
         ("text-align: center", {"alignment": {"horizontal": "center"}}),
@@ -249,6 +261,20 @@ def test_css_to_excel_multiple():
     } == actual
 
 
+@pytest.mark.parametrize(
+    "css",
+    [
+        "border-top-style: unhandled-border-style",
+        "border-style: another-unhandled-style",
+    ],
+)
+def test_css_to_excel_unhandled_border_style_warns(css):
+    """Test that unhandled border styles raise a CSSWarning."""
+    convert = CSSToExcelConverter()
+    with tm.assert_produces_warning(CSSWarning, match="Unhandled border style format"):
+        convert(css)
+
+
 @pytest.mark.parametrize(
     "css,inherited,expected",
     [
@@ -330,6 +356,23 @@ def test_css_to_excel_bad_colors(input_color):
         assert expected == convert(css)
 
 
+@pytest.mark.parametrize("input_color", ["#", "#1234567"])
+def test_css_to_excel_invalid_color_raises(input_color):
+    """Test that invalid colors raise a ValueError."""
+    css = (
+        f"border-top-color: {input_color}; "
+        f"border-right-color: {input_color}; "
+        f"border-bottom-color: {input_color}; "
+        f"border-left-color: {input_color}; "
+        f"background-color: {input_color}; "
+        f"color: {input_color}"
+    )
+
+    convert = CSSToExcelConverter()
+    with pytest.raises(ValueError, match=f"Unexpected color {input_color}"):
+        convert(css)
+
+
 def tests_css_named_colors_valid():
     upper_hexs = set(map(str.upper, string.hexdigits))
     for color in CSSToExcelConverter.NAMED_COLORS.values():
diff --git a/pandas/tests/io/test_stata.py b/pandas/tests/io/test_stata.py
index e73de78847..b155c0cca4 100644
--- a/pandas/tests/io/test_stata.py
+++ b/pandas/tests/io/test_stata.py
@@ -2601,3 +2601,13 @@ def test_strl_missings(temp_file, version):
         ]
     )
     df.to_stata(temp_file, version=version)
+
+
+@pytest.mark.parametrize("version", [117, 118, 119, None])
+def test_ascii_error(temp_file, version):
+    # GH #61583
+    # Check that 2 byte long unicode characters doesn't cause export error
+    df = DataFrame({"doubleByteCol": ["§" * 1500]})
+    df.to_stata(temp_file, write_index=0, version=version)
+    df_input = read_stata(temp_file)
+    tm.assert_frame_equal(df, df_input)
diff --git a/pandas/tests/series/methods/test_explode.py b/pandas/tests/series/methods/test_explode.py
index e4ad2493f9..9c08f47c0d 100644
--- a/pandas/tests/series/methods/test_explode.py
+++ b/pandas/tests/series/methods/test_explode.py
@@ -175,3 +175,11 @@ def test_explode_pyarrow_non_list_type(ignore_index):
     result = ser.explode(ignore_index=ignore_index)
     expected = pd.Series([1, 2, 3], dtype="int64[pyarrow]", index=[0, 1, 2])
     tm.assert_series_equal(result, expected)
+
+
+def test_str_dtype():
+    # https://github.com/pandas-dev/pandas/pull/61623
+    ser = pd.Series(["x", "y"], dtype="str")
+    result = ser.explode()
+    assert result is not ser
+    tm.assert_series_equal(result, ser)