Repository: pandas-dev/pandas (python)

Issue:
DOC: Typo in docs for na_values parameter in pandas.read_csv function
### Pandas version checks

- [X] I have checked that the issue still exists on the latest versions of the docs on `main` [here](https://pandas.pydata.org/docs/dev/)


### Location of the documentation

https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html

### Documentation problem

Hi,
I was looking through the pandas documentation for the `na_values` parameter under `pandas.read_csv` and I noticed that there's a typo in the section:

>   By default the following values are interpreted as NaN: “ “, “#N/A”, “#N/A N/A”, “#NA”, “-1.#IND”, “-1.#QNAN”, “-NaN”, “-nan”, “1.#IND”, “1.#QNAN”, “<NA>”, “N/A”, “NA”, “NULL”, “NaN”, “None”, “n/a”, “nan”, “null “.


The `" "` is included in the list of default string to NAs starting in pandas 2.1 all the way to the current version of the docs. However, [in the code for the list of default string to NAs](https://github.com/pandas-dev/pandas/blob/main/pandas/_libs/parsers.pyx#L1373-L1393), I'm seeing `""` instead.

### Suggested fix for documentation

I think the fix should be to remove `" "` and replace with `""` in the  list of default string to NAs in the pandas docs for the versions 2.1 and up to the current version so it's consistent with the code.

Thanks for all your help!

Pull request description:
 DOC: Typo in docs for na_values parameter in pandas.read_csv function #59314 
Fixed the documentation of `read_csv` to list the correct default list of `na_values`.
- [x] closes #59314
- [ ] [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/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 1627a90fc6..e3c4e69db7 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -589,7 +589,7 @@ I/O
 
 Period
 ^^^^^^
--
+- Fixed error message when passing invalid period alias to :meth:`PeriodIndex.to_timestamp` (:issue:`58974`)
 -
 
 Plotting
diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx
index ff24c2942c..fd1bb3fe3e 100644
--- a/pandas/_libs/tslibs/offsets.pyx
+++ b/pandas/_libs/tslibs/offsets.pyx
@@ -4752,20 +4752,16 @@ def _validate_to_offset_alias(alias: str, is_period: bool) -> None:
                 alias.lower() not in {"s", "ms", "us", "ns"} and
                 alias.upper().split("-")[0].endswith(("S", "E"))):
             raise ValueError(INVALID_FREQ_ERR_MSG.format(alias))
-    if (is_period and
-            alias.upper() in c_OFFSET_TO_PERIOD_FREQSTR and
-            alias != "ms" and
-            alias.upper().split("-")[0].endswith(("S", "E"))):
-        if (alias.upper().startswith("B") or
-                alias.upper().startswith("S") or
-                alias.upper().startswith("C")):
-            raise ValueError(INVALID_FREQ_ERR_MSG.format(alias))
-        else:
-            alias_msg = "".join(alias.upper().split("E", 1))
-            raise ValueError(
-                f"for Period, please use \'{alias_msg}\' "
-                f"instead of \'{alias}\'"
-            )
+    if (
+        is_period and
+        alias in c_OFFSET_TO_PERIOD_FREQSTR and
+        alias != c_OFFSET_TO_PERIOD_FREQSTR[alias]
+    ):
+        alias_msg = c_OFFSET_TO_PERIOD_FREQSTR.get(alias)
+        raise ValueError(
+            f"for Period, please use \'{alias_msg}\' "
+            f"instead of \'{alias}\'"
+        )
 
 
 # TODO: better name?
diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py
index 352020f453..e62cda0dfe 100644
--- a/pandas/core/config_init.py
+++ b/pandas/core/config_init.py
@@ -14,6 +14,7 @@ from __future__ import annotations
 
 from collections.abc import Callable
 import os
+from typing import Any
 
 import pandas._config.config as cf
 from pandas._config.config import (
@@ -455,12 +456,27 @@ string_storage_doc = """
     ``future.infer_string`` is set to True.
 """
 
+
+def is_valid_string_storage(value: Any) -> None:
+    legal_values = ["python", "pyarrow"]
+    if value not in legal_values:
+        msg = "Value must be one of python|pyarrow"
+        if value == "pyarrow_numpy":
+            # TODO: we can remove extra message after 3.0
+            msg += (
+                ". 'pyarrow_numpy' was specified, but this option should be "
+                "enabled using pandas.options.future.infer_string instead"
+            )
+        raise ValueError(msg)
+
+
 with cf.config_prefix("mode"):
     cf.register_option(
         "string_storage",
         "python",
         string_storage_doc,
-        validator=is_one_of_factory(["python", "pyarrow", "pyarrow_numpy"]),
+        # validator=is_one_of_factory(["python", "pyarrow"]),
+        validator=is_valid_string_storage,
     )
 
 
diff --git a/pandas/io/parsers/readers.py b/pandas/io/parsers/readers.py
index c28d3aaaf4..0cca1ebdb8 100644
--- a/pandas/io/parsers/readers.py
+++ b/pandas/io/parsers/readers.py
@@ -142,8 +142,7 @@ else:
     _read_shared = dict
 
 
-_doc_read_csv_and_table = (
-    r"""
+_doc_read_csv_and_table = r"""
 {summary}
 
 Also supports optionally iterating or breaking of the file
@@ -272,10 +271,7 @@ nrows : int, optional
 na_values : Hashable, Iterable of Hashable or dict of {{Hashable : Iterable}}, optional
     Additional strings to recognize as ``NA``/``NaN``. If ``dict`` passed, specific
     per-column ``NA`` values.  By default the following values are interpreted as
-    ``NaN``: " """
-    + fill('", "'.join(sorted(STR_NA_VALUES)), 70, subsequent_indent="    ")
-    + """ ".
-
+    ``NaN``: "{na_values_str}".
 keep_default_na : bool, default True
     Whether or not to include the default ``NaN`` values when parsing the data.
     Depending on whether ``na_values`` is passed in, the behavior is as follows:
@@ -357,8 +353,7 @@ lineterminator : str (length 1), optional
 quotechar : str (length 1), optional
     Character used to denote the start and end of a quoted item. Quoted
     items can include the ``delimiter`` and it will be ignored.
-quoting : {{0 or csv.QUOTE_MINIMAL, 1 or csv.QUOTE_ALL, 2 or csv.QUOTE_NONNUMERIC, \
-3 or csv.QUOTE_NONE}}, default csv.QUOTE_MINIMAL
+quoting : {{0 or csv.QUOTE_MINIMAL, 1 or csv.QUOTE_ALL, 2 or csv.QUOTE_NONNUMERIC, 3 or csv.QUOTE_NONE}}, default csv.QUOTE_MINIMAL
     Control field quoting behavior per ``csv.QUOTE_*`` constants. Default is
     ``csv.QUOTE_MINIMAL`` (i.e., 0) which implies that only fields containing special
     characters are quoted (e.g., characters defined in ``quotechar``, ``delimiter``,
@@ -544,8 +539,7 @@ col 1             int64
 col 2    datetime64[ns]
 col 3    datetime64[ns]
 dtype: object
-"""
-)
+"""  # noqa: E501
 
 
 class _C_Parser_Defaults(TypedDict):
@@ -756,6 +750,9 @@ def read_csv(
         summary="Read a comma-separated values (csv) file into DataFrame.",
         see_also_func_name="read_table",
         see_also_func_summary="Read general delimited file into DataFrame.",
+        na_values_str=fill(
+            '", "'.join(sorted(STR_NA_VALUES)), 70, subsequent_indent="    "
+        ),
         _default_sep="','",
         storage_options=_shared_docs["storage_options"],
         decompression_options=_shared_docs["decompression_options"]
@@ -888,6 +885,9 @@ def read_table(
         see_also_func_summary=(
             "Read a comma-separated values (csv) file into DataFrame."
         ),
+        na_values_str=fill(
+            '", "'.join(sorted(STR_NA_VALUES)), 70, subsequent_indent="    "
+        ),
         _default_sep=r"'\\t' (tab-stop)",
         storage_options=_shared_docs["storage_options"],
         decompression_options=_shared_docs["decompression_options"]


Test patch:
diff --git a/pandas/conftest.py b/pandas/conftest.py
index 512f6429be..11196ad069 100644
--- a/pandas/conftest.py
+++ b/pandas/conftest.py
@@ -1296,7 +1296,6 @@ def nullable_string_dtype(request):
     params=[
         "python",
         pytest.param("pyarrow", marks=td.skip_if_no("pyarrow")),
-        pytest.param("pyarrow_numpy", marks=td.skip_if_no("pyarrow")),
     ]
 )
 def string_storage(request):
@@ -1305,7 +1304,6 @@ def string_storage(request):
 
     * 'python'
     * 'pyarrow'
-    * 'pyarrow_numpy'
     """
     return request.param
 
diff --git a/pandas/tests/arrays/string_/test_string.py b/pandas/tests/arrays/string_/test_string.py
index d741d6da43..293f3c7422 100644
--- a/pandas/tests/arrays/string_/test_string.py
+++ b/pandas/tests/arrays/string_/test_string.py
@@ -514,19 +514,12 @@ def test_arrow_array(dtype):
     assert arr.equals(expected)
 
 
-@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False)
+@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)")
 @pytest.mark.filterwarnings("ignore:Passing a BlockManager:DeprecationWarning")
-def test_arrow_roundtrip(dtype, string_storage2, request, using_infer_string):
+def test_arrow_roundtrip(dtype, string_storage, using_infer_string):
     # roundtrip possible from arrow 1.0.0
     pa = pytest.importorskip("pyarrow")
 
-    if using_infer_string and string_storage2 != "pyarrow_numpy":
-        request.applymarker(
-            pytest.mark.xfail(
-                reason="infer_string takes precedence over string storage"
-            )
-        )
-
     data = pd.array(["a", "b", None], dtype=dtype)
     df = pd.DataFrame({"a": data})
     table = pa.table(df)
@@ -534,30 +527,21 @@ def test_arrow_roundtrip(dtype, string_storage2, request, using_infer_string):
         assert table.field("a").type == "string"
     else:
         assert table.field("a").type == "large_string"
-    with pd.option_context("string_storage", string_storage2):
+    with pd.option_context("string_storage", string_storage):
         result = table.to_pandas()
     assert isinstance(result["a"].dtype, pd.StringDtype)
-    expected = df.astype(f"string[{string_storage2}]")
+    expected = df.astype(f"string[{string_storage}]")
     tm.assert_frame_equal(result, expected)
     # ensure the missing value is represented by NA and not np.nan or None
     assert result.loc[2, "a"] is result["a"].dtype.na_value
 
 
-@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False)
+@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)")
 @pytest.mark.filterwarnings("ignore:Passing a BlockManager:DeprecationWarning")
-def test_arrow_load_from_zero_chunks(
-    dtype, string_storage2, request, using_infer_string
-):
+def test_arrow_load_from_zero_chunks(dtype, string_storage, using_infer_string):
     # GH-41040
     pa = pytest.importorskip("pyarrow")
 
-    if using_infer_string and string_storage2 != "pyarrow_numpy":
-        request.applymarker(
-            pytest.mark.xfail(
-                reason="infer_string takes precedence over string storage"
-            )
-        )
-
     data = pd.array([], dtype=dtype)
     df = pd.DataFrame({"a": data})
     table = pa.table(df)
@@ -567,10 +551,10 @@ def test_arrow_load_from_zero_chunks(
         assert table.field("a").type == "large_string"
     # Instantiate the same table with no chunks at all
     table = pa.table([pa.chunked_array([], type=pa.string())], schema=table.schema)
-    with pd.option_context("string_storage", string_storage2):
+    with pd.option_context("string_storage", string_storage):
         result = table.to_pandas()
     assert isinstance(result["a"].dtype, pd.StringDtype)
-    expected = df.astype(f"string[{string_storage2}]")
+    expected = df.astype(f"string[{string_storage}]")
     tm.assert_frame_equal(result, expected)
 
 
diff --git a/pandas/tests/arrays/string_/test_string_arrow.py b/pandas/tests/arrays/string_/test_string_arrow.py
index 0e0db74a37..65c6ce8e9c 100644
--- a/pandas/tests/arrays/string_/test_string_arrow.py
+++ b/pandas/tests/arrays/string_/test_string_arrow.py
@@ -27,16 +27,18 @@ def test_eq_all_na():
 
 
 def test_config(string_storage, request, using_infer_string):
-    if using_infer_string and string_storage != "pyarrow_numpy":
-        request.applymarker(pytest.mark.xfail(reason="infer string takes precedence"))
-    if string_storage == "pyarrow_numpy":
+    if using_infer_string and string_storage == "python":
+        # python string storage with na_value=NaN is not yet implemented
         request.applymarker(pytest.mark.xfail(reason="TODO(infer_string)"))
+
     with pd.option_context("string_storage", string_storage):
         assert StringDtype().storage == string_storage
         result = pd.array(["a", "b"])
         assert result.dtype.storage == string_storage
 
-    dtype = StringDtype(string_storage)
+    dtype = StringDtype(
+        string_storage, na_value=np.nan if using_infer_string else pd.NA
+    )
     expected = dtype.construct_array_type()._from_sequence(["a", "b"], dtype=dtype)
     tm.assert_equal(result, expected)
 
diff --git a/pandas/tests/frame/methods/test_astype.py b/pandas/tests/frame/methods/test_astype.py
index 0b525c8d9e..c6c702a1a0 100644
--- a/pandas/tests/frame/methods/test_astype.py
+++ b/pandas/tests/frame/methods/test_astype.py
@@ -897,3 +897,12 @@ def test_astype_to_string_not_modifying_input(string_storage, val):
     with option_context("mode.string_storage", string_storage):
         df.astype("string")
     tm.assert_frame_equal(df, expected)
+
+
+@pytest.mark.parametrize("val", [None, 1, 1.5, np.nan, NaT])
+def test_astype_to_string_dtype_not_modifying_input(any_string_dtype, val):
+    # GH#51073 - variant of the above test with explicit dtype instances
+    df = DataFrame({"a": ["a", "b", val]})
+    expected = df.copy()
+    df.astype(any_string_dtype)
+    tm.assert_frame_equal(df, expected)
diff --git a/pandas/tests/frame/methods/test_convert_dtypes.py b/pandas/tests/frame/methods/test_convert_dtypes.py
index 91fa81b5be..59779234b4 100644
--- a/pandas/tests/frame/methods/test_convert_dtypes.py
+++ b/pandas/tests/frame/methods/test_convert_dtypes.py
@@ -10,6 +10,8 @@ import pandas._testing as tm
 
 
 class TestConvertDtypes:
+    # TODO convert_dtypes should not use NaN variant of string dtype, but always NA
+    @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)")
     @pytest.mark.parametrize(
         "convert_integer, expected", [(False, np.dtype("int32")), (True, "Int32")]
     )
@@ -18,9 +20,6 @@ class TestConvertDtypes:
     ):
         # Specific types are tested in tests/series/test_dtypes.py
         # Just check that it works for DataFrame here
-        if using_infer_string:
-            string_storage = "pyarrow_numpy"
-
         df = pd.DataFrame(
             {
                 "a": pd.Series([1, 2, 3], dtype=np.dtype("int32")),
diff --git a/pandas/tests/indexes/period/methods/test_to_timestamp.py b/pandas/tests/indexes/period/methods/test_to_timestamp.py
index 3867f9e324..4fe429ce71 100644
--- a/pandas/tests/indexes/period/methods/test_to_timestamp.py
+++ b/pandas/tests/indexes/period/methods/test_to_timestamp.py
@@ -140,3 +140,10 @@ class TestToTimestamp:
 
         result = index.to_timestamp()
         assert result[0] == Timestamp("1/1/2012")
+
+
+def test_ms_to_timestamp_error_message():
+    # https://github.com/pandas-dev/pandas/issues/58974#issuecomment-2164265446
+    ix = period_range("2000", periods=3, freq="M")
+    with pytest.raises(ValueError, match="for Period, please use 'M' instead of 'MS'"):
+        ix.to_timestamp("MS")
diff --git a/pandas/tests/io/conftest.py b/pandas/tests/io/conftest.py
index ab6cacc4cc..bdefadf3db 100644
--- a/pandas/tests/io/conftest.py
+++ b/pandas/tests/io/conftest.py
@@ -224,19 +224,3 @@ def compression_format(request):
 @pytest.fixture(params=_compression_formats_params)
 def compression_ext(request):
     return request.param[0]
-
-
-@pytest.fixture(
-    params=[
-        "python",
-        pytest.param("pyarrow", marks=td.skip_if_no("pyarrow")),
-    ]
-)
-def string_storage(request):
-    """
-    Parametrized fixture for pd.options.mode.string_storage.
-
-    * 'python'
-    * 'pyarrow'
-    """
-    return request.param