Repository: pandas-dev/pandas (Python)

Issue:
REGR: change in Series.astype(str) behavior for None
#### Question about pandas

Let's say we have a `Series` with `None` like the below.

```python
pser = pd.Series(["hi", "hi ", " ", " \t", "", None], name="x")

print(pser)
0      hi
1     hi
2
3      \t
4
5    None
Name: x, dtype: object
```

the last value is `None`, but it is casted to `nan` after using `astype` with `str` parameter.

```python
print(pser.astype(str))
0     hi
1    hi
2
3     \t
4
5    nan
Name: x, dtype: object
```

Is it intended behavior in pandas 1.1.1. ??

Thanks :)


Discussion:
Thanks @itholic, this seems like a regression and is due to https://github.com/pandas-dev/pandas/pull/35519

cc @topper-123 
Thanks, @dsaxton :)



Patch:
diff --git a/doc/source/whatsnew/v1.1.4.rst b/doc/source/whatsnew/v1.1.4.rst
index 3ad8d981be2c9..ca58239dccb8d 100644
--- a/doc/source/whatsnew/v1.1.4.rst
+++ b/doc/source/whatsnew/v1.1.4.rst
@@ -17,6 +17,7 @@ Fixed regressions
 - Fixed regression where attempting to mutate a :class:`DateOffset` object would no longer raise an ``AttributeError`` (:issue:`36940`)
 - Fixed regression where :meth:`DataFrame.agg` would fail with :exc:`TypeError` when passed positional arguments to be passed on to the aggregation function (:issue:`36948`).
 - Fixed regression in :class:`RollingGroupby` with ``sort=False`` not being respected (:issue:`36889`)
+- Fixed regression in :meth:`Series.astype` converting ``None`` to ``"nan"`` when casting to string (:issue:`36904`)
 
 .. ---------------------------------------------------------------------------
 
diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py
index 48391ab7d9373..e8b72b997cd5d 100644
--- a/pandas/core/dtypes/cast.py
+++ b/pandas/core/dtypes/cast.py
@@ -921,7 +921,9 @@ def astype_nansafe(arr, dtype, copy: bool = True, skipna: bool = False):
         dtype = pandas_dtype(dtype)
 
     if issubclass(dtype.type, str):
-        return lib.ensure_string_array(arr.ravel(), skipna=skipna).reshape(arr.shape)
+        return lib.ensure_string_array(
+            arr.ravel(), skipna=skipna, convert_na_value=False
+        ).reshape(arr.shape)
 
     elif is_datetime64_dtype(arr):
         if is_object_dtype(dtype):


Test patch:
diff --git a/pandas/tests/series/methods/test_astype.py b/pandas/tests/series/methods/test_astype.py
index 7449d8d65ef96..eea839c380f0b 100644
--- a/pandas/tests/series/methods/test_astype.py
+++ b/pandas/tests/series/methods/test_astype.py
@@ -1,7 +1,7 @@
 import numpy as np
 import pytest
 
-from pandas import Interval, Series, Timestamp, date_range
+from pandas import NA, Interval, Series, Timestamp, date_range
 import pandas._testing as tm
 
 
@@ -55,3 +55,18 @@ def test_astype_from_float_to_str(self, dtype):
         result = s.astype(str)
         expected = Series(["0.1"])
         tm.assert_series_equal(result, expected)
+
+    @pytest.mark.parametrize(
+        "value, string_value",
+        [
+            (None, "None"),
+            (np.nan, "nan"),
+            (NA, "<NA>"),
+        ],
+    )
+    def test_astype_to_str_preserves_na(self, value, string_value):
+        # https://github.com/pandas-dev/pandas/issues/36904
+        s = Series(["a", "b", value], dtype=object)
+        result = s.astype(str)
+        expected = Series(["a", "b", string_value], dtype=object)
+        tm.assert_series_equal(result, expected)