Repository: pandas-dev/pandas
Pull request: API: more permissive conversion to StringDtype
- [x] closes #31204
- [ ] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [ ] whatsnew entry

This is a proposal to make using ``StringDtype`` more permissive and be usable inplace of ``dtype=str``.

ATM converting to StringDtype will only accept arrays that are ``str`` already, meaning you will often have to use ``astype(str).astype("string")`` to be sure not to get errors, which can be tedious. For example these fail in master and work in this PR:

```python
>>> pd.Series([1,2, np.nan], dtype="string")
0       1
1       2
2    <NA>
dtype: string
>>> pd.array([1,2, np.nan], dtype="string")
<StringArray>
['1', '2', <NA>]
Length: 3, dtype: string
>>> pd.Series([1,2, np.nan]).astype("string")
0     1.0
1     2.0
2    <NA>
dtype: string
>>> pd.Series([1,2, np.nan], dtype="Int64").astype("string")
0       1
1       2
2    <NA>
dtype: string
```

etc. now work. Previously the above all gave errors.

## The proposed solution:
``ExtensionArray._from_sequence`` is in master explicitly stated to expect a sequence of scalars of the *correct* type. This makes it not usable for type conversion.

I've therefore added a new ``ExtensionArray._from_sequence_of_any_type`` that accepts scalars of any type and may change the scalars to the correct type before passing them on to ``_from_sequence``. Currently it just routes everything through to ``ExtensionArray._from_sequence``, except in ``StringArray._from_sequence_of_any_type``, where it massages the input scalars into strings before passing them on.

Obviously tests and doc updates are still missing, but I would appreciate feedback if this solution looks ok and I'll add tests and doc updates if this is ok.

Problem statement:
convert numeric column to dedicated `pd.StringDtype()`
#### Code Sample, a copy-pastable example if possible

```python
pd.Series(range(5, 10), dtype="Int64").astype("string")
```
raises `TypeError: data type not understood`

while 

```python
pd.Series(range(5, 10)).astype("string")
```
raises `ValueError: StringArray requires a sequence of strings or missing values.`


If you first do `astype(str)`: 
```python
pd.Series(range(5, 10)).astype(str).astype("string")
```
and 
```python
pd.Series(range(5, 10), dtype="Int64").astype(str).astype("string")
```
work as expected:


```
0    5
1    6
2    7
3    8
4    9
dtype: string
```

While `astype(object)` raises in both cases `ValueError: StringArray requires a sequence of strings or missing values.`

#### Problem description

I can understand the `ValueError`, since you don't feed strings to the `StringArray`. Best for me would be if the `astype("string")` converts it to strings, or if the `astype(str)` would return a `StringArray`, but in any case, I would expect both `pd.Series(range(5, 10), dtype="Int64").astype("string")` and `pd.Series(range(5, 10)).astype("string")` to raise the same error.

#### Expected Output

```
0    5
1    6
2    7
3    8
4    9
dtype: string
```
 
or 

 `ValueError: StringArray requires a sequence of strings or missing values.`

#### Output of ``pd.show_versions()``

<details>

[paste the output of ``pd.show_versions()`` here below this line]

</details>

Hints from issue discussion:
Overlaps with https://github.com/pandas-dev/pandas/issues/22384, which is trying to solve this problem in general.

In the meantime, we can add support for this in IntegerArray.astype.
@TomAugspurger Was skimming issues and saw this one.

I'm wondering if `Series.astype('string')` should be treated as a special case independent of the underlying dtype of the underlying Series.  That's because the following code should always work assuming `s` is a `Series`:

```python
pd.Series([str(x) if not pd.isna(x) else pd.NA for x in s], dtype="string")
```

So since we know that the underlying objects in the EA have to support `str()`, there is a straightforward way of doing that conversion.

If you agree, I can look into doing a PR



I don't think we should preempt the array from having a chance to perform
the conversion.

And code-wise, I don't think we'd want a special case for this in
NDFrame.astype.

On Fri, Jan 24, 2020 at 10:19 AM Irv Lustig <notifications@github.com>
wrote:

> @TomAugspurger <https://github.com/TomAugspurger> Was skimming issues and
> saw this one.
>
> I'm wondering if Series.astype('string') should be treated as a special
> case independent of the underlying dtype of the underlying Series. That's
> because the following code should always work assuming s is a Series:
>
> pd.Series([str(x) if not pd.isna(x) else pd.NA for x in s], dtype="string")
>
> So since we know that the underlying objects in the EA have to support
> str(), there is a straightforward way of doing that conversion.
>
> If you agree, I can look into doing a PR
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/pandas-dev/pandas/issues/31204?email_source=notifications&email_token=AAKAOIWWCMHG5VDJ2ALQ6KTQ7MIHNA5CNFSM4KKFMCQ2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEJ3JVCQ#issuecomment-578198154>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAKAOIWKL7DYNRJPOCG5XFLQ7MIHNANCNFSM4KKFMCQQ>
> .
>

> And code-wise, I don't think we'd want a special case for this in NDFrame.astype.

Well, I'm suggesting that we _do_ want a special case just for `StringDtype` in `NDFrame.astype`.  It seems natural that the 2 operations below should produce the same result, modulo having an `object` vs `string` dtype, for a given Series `s` (and returning `np.nan` vs. `pd.NA` for missing values), independent of the dtype of the Series `s`:
```
s.astype(str)
s.astype('string')
```


Right, that's definitely desirable. But I don't think NDFrame.astype is the place for the fix.
For example, consider SparseArray. It implements astype such that the result is also a SparseArray and preserves the sparsity. So `Series[SparseArray].astype("string")` would return a `Series[SparseArray[string]]`. But NDFrame.astype has no awareness of that.
> For example, consider SparseArray. It implements astype such that the result is also a SparseArray and preserves the sparsity. So `Series[SparseArray].astype("string")` would return a `Series[SparseArray[string]]`. But NDFrame.astype has no awareness of that.

I guess this depends on the semantics of `astype()` in the following sense.

If I have an EA  of type "current_dtype" and I write `s.astype("target_dtype")`, is it either:

1. the responsibility of the EA with type "current_dtype" to know how to convert to every possible "target_dtype", or
2. the responsibility of EA's of type "target_dtype" to know how to convert whatever types it can to "target_dtype"?

I think you are saying that the design we have supports (1), and I'm suggesting a design corresponding to (2).

Now, the reason that I prefer (2) is that when I construct a `Series`, and I provide the `dtype` as an argument, then pandas figures out how to convert the data passed to the Series to the corresponding dtype if it can.  That is behavior corresponding to (2).  So `s=pd.Series([1,2,3], dtype="category")`  and `s=pd.Series([1,0,pd.NA], "boolean")` both work, but `s=pd.Series([1,2,3], dtype="string")` does not.

Another possible design would be to have a property of EA's called something like `can_convert_anydtype` being `True` or `False`, and if `True`, then `astype` knows it can ask the EA to convert any dtype, and if `False`, it then asks the target dtype to do the conversion.  So, for `StringDtype`, we set it to be `can_convert_anydtype` to `True`, and for other dtypes set it to `False`


We have another issue for an astype dispatch mechanism.
I want to chime in just to give another use-case from the duplicated issue #31839 .

In addition to conversion **to** `"string"`, converting `"string"` **to** `"Int8/16/64"` when the initial series contains pd.NA is currently quite tricky:

```python
s = pd.Series(['0', pd.NA], dtype='string')
s.astype('object').replace(pd.NA, np.nan).astype('float64').astype('Int8')
```
It should be possible to do simply `s.astype('Int8')`
> 
> 
> I want to chime in just to give another use-case from the duplicated issue #31839 .
> 
> In addition to conversion **to** `"string"`, converting `"string"` **to** `"Int8/16/64"` when the initial series contains pd.NA is currently quite tricky:
> 
> ```python
> s = pd.Series(['0', pd.NA], dtype='string')
> s.astype('object').replace(pd.NA, np.nan).astype('float64').astype('Int8')
> ```
> 
> It should be possible to do simply `s.astype('Int8')`

Your solution can give rounding errors when dealing with large integers. (I've been bitten by this when importing production data. The batch numbers were too large to fit exactly in a float64)

```python
s = pd.Series(["0", pd.NA, str(2 ** 60 + 2)], dtype="string")
s.to_frame().assign(
    a=s.astype("object")
    .replace(pd.NA, np.nan)
    .astype("float64")
    .astype("Int64"),
    b=s.apply(lambda x: int(x) if pd.notnull(x) else x).astype("Int64"),
)
```

  | 0 | a | b
-- | -- | -- | --
0 | 0 | 0 | 0
1 | <NA> | <NA> | <NA>
2 | 1152921504606846978 | 1152921504606846976 | 1152921504606846978


This explicitly loops over the column, so is not ideal performance wise

Commit message: update doc string

Patch to review:
diff --git a/doc/source/user_guide/text.rst b/doc/source/user_guide/text.rst
index bea0f42f6849c..4f57a7c2825cf 100644
--- a/doc/source/user_guide/text.rst
+++ b/doc/source/user_guide/text.rst
@@ -63,6 +63,27 @@ Or ``astype`` after the ``Series`` or ``DataFrame`` is created
    s
    s.astype("string")
 
+
+.. versionchanged:: 1.1.0
+
+You can also use ``string`` dtype on non-string data and it will be converted to
+``string`` dtype:
+
+.. ipython:: python
+
+   s = pd.Series(['a', 2, np.nan], dtype="string")
+   s
+   type(s[1])
+
+or convert from existing pandas data:
+
+   s1 = pd.Series([1,2, np.nan], dtype="Int64")
+   s1
+   s2 = s1.astype("string")
+   s2
+   type(s2[0])
+
+
 .. _text.differences:
 
 Behavior differences
diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst
index 20e2cce1a3dfa..4be1393f906df 100644
--- a/doc/source/whatsnew/v1.1.0.rst
+++ b/doc/source/whatsnew/v1.1.0.rst
@@ -13,6 +13,24 @@ including other versions of pandas.
 Enhancements
 ~~~~~~~~~~~~
 
+.. _whatsnew_110.astype_string:
+
+All dtypes can now be converted to ``StringDtype``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Previously, declaring or converting to :class:`StringDtype` was in general only possible if the data was already only ``str`` or nan-like (:issue:`31204`).
+:class:`StringDtype` now works in all situations where ``astype(str)`` or ``dtype=str`` work:
+
+For example the below now work:
+
+.. ipython:: python
+
+   ser = pd.Series([1, "abc", np.nan], dtype="string")
+   ser
+   ser[0]
+   pd.Series([1, 2, np.nan], dtype="Int64").astype("string")
+
+
 .. _whatsnew_110.period_index_partial_string_slicing:
 
 Nonmonotonic PeriodIndex Partial String Slicing
diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py
index fb9e2f6732018..b5e917bafca7e 100644
--- a/pandas/core/arrays/base.py
+++ b/pandas/core/arrays/base.py
@@ -20,7 +20,7 @@
 from pandas.util._validators import validate_fillna_kwargs
 
 from pandas.core.dtypes.cast import maybe_cast_to_extension_array
-from pandas.core.dtypes.common import is_array_like, is_list_like
+from pandas.core.dtypes.common import is_array_like, is_list_like, pandas_dtype
 from pandas.core.dtypes.dtypes import ExtensionDtype
 from pandas.core.dtypes.generic import ABCIndexClass, ABCSeries
 from pandas.core.dtypes.missing import isna
@@ -178,7 +178,7 @@ def _from_sequence(cls, scalars, dtype=None, copy=False):
         ----------
         scalars : Sequence
             Each element will be an instance of the scalar type for this
-            array, ``cls.dtype.type``.
+            array, ``cls.dtype.type`` or be converted into this type in this method.
         dtype : dtype, optional
             Construct for this particular dtype. This should be a Dtype
             compatible with the ExtensionArray.
@@ -451,6 +451,12 @@ def astype(self, dtype, copy=True):
         array : ndarray
             NumPy ndarray with 'dtype' for its dtype.
         """
+        from pandas.core.arrays.string_ import StringDtype
+
+        dtype = pandas_dtype(dtype)
+        if isinstance(dtype, StringDtype):  # allow conversion to StringArrays
+            return dtype.construct_array_type()._from_sequence(self, copy=False)
+
         return np.array(self, dtype=dtype, copy=copy)
 
     def isna(self) -> ArrayLike:
diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py
index 3c1602344c314..cf3cde155a3bb 100644
--- a/pandas/core/arrays/datetimelike.py
+++ b/pandas/core/arrays/datetimelike.py
@@ -27,6 +27,7 @@
     is_datetime64tz_dtype,
     is_datetime_or_timedelta_dtype,
     is_dtype_equal,
+    is_extension_array_dtype,
     is_float_dtype,
     is_integer_dtype,
     is_list_like,
@@ -619,7 +620,11 @@ def astype(self, dtype, copy=True):
         if is_object_dtype(dtype):
             return self._box_values(self.asi8.ravel()).reshape(self.shape)
         elif is_string_dtype(dtype) and not is_categorical_dtype(dtype):
-            return self._format_native_types()
+            if is_extension_array_dtype(dtype):
+                arr_cls = dtype.construct_array_type()
+                return arr_cls._from_sequence(self, dtype=dtype)
+            else:
+                return self._format_native_types()
         elif is_integer_dtype(dtype):
             # we deliberately ignore int32 vs. int64 here.
             # See https://github.com/pandas-dev/pandas/issues/24381 for more.
diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py
index 59954f548fd33..d4137f9666946 100644
--- a/pandas/core/arrays/integer.py
+++ b/pandas/core/arrays/integer.py
@@ -1,5 +1,5 @@
 import numbers
-from typing import TYPE_CHECKING, List, Optional, Tuple, Type, Union
+from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union
 import warnings
 
 import numpy as np
@@ -442,17 +442,20 @@ def astype(self, dtype, copy: bool = True) -> ArrayLike:
             if incompatible type with an IntegerDtype, equivalent of same_kind
             casting
         """
-        from pandas.core.arrays.boolean import BooleanArray, BooleanDtype
+        from pandas.core.arrays.boolean import BooleanDtype
+        from pandas.core.arrays.string_ import StringDtype
 
         dtype = pandas_dtype(dtype)
 
         # if we are astyping to an existing IntegerDtype we can fastpath
         if isinstance(dtype, _IntegerDtype):
             result = self._data.astype(dtype.numpy_dtype, copy=False)
-            return type(self)(result, mask=self._mask, copy=False)
+            return dtype.construct_array_type()(result, mask=self._mask, copy=False)
         elif isinstance(dtype, BooleanDtype):
             result = self._data.astype("bool", copy=False)
-            return BooleanArray(result, mask=self._mask, copy=False)
+            return dtype.construct_array_type()(result, mask=self._mask, copy=False)
+        elif isinstance(dtype, StringDtype):
+            return dtype.construct_array_type()._from_sequence(self, copy=False)
 
         # coerce
         if is_float_dtype(dtype):
@@ -722,7 +725,7 @@ class UInt64Dtype(_IntegerDtype):
     __doc__ = _dtype_docstring.format(dtype="uint64")
 
 
-_dtypes = {
+_dtypes: Dict[str, _IntegerDtype] = {
     "int8": Int8Dtype(),
     "int16": Int16Dtype(),
     "int32": Int32Dtype(),
diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py
index c5366884fbdfe..c861d25afd13f 100644
--- a/pandas/core/arrays/interval.py
+++ b/pandas/core/arrays/interval.py
@@ -680,8 +680,11 @@ def astype(self, dtype, copy=True):
         array : ExtensionArray or ndarray
             ExtensionArray or NumPy ndarray with 'dtype' for its dtype.
         """
+        from pandas.core.arrays.string_ import StringDtype
+
         if dtype is not None:
             dtype = pandas_dtype(dtype)
+
         if is_interval_dtype(dtype):
             if dtype == self.dtype:
                 return self.copy() if copy else self
@@ -698,6 +701,9 @@ def astype(self, dtype, copy=True):
             return self._shallow_copy(new_left, new_right)
         elif is_categorical_dtype(dtype):
             return Categorical(np.asarray(self))
+        elif isinstance(dtype, StringDtype):
+            return dtype.construct_array_type()._from_sequence(self, copy=False)
+
         # TODO: This try/except will be repeated.
         try:
             return np.asarray(self).astype(dtype, copy=copy)
diff --git a/pandas/core/arrays/sparse/dtype.py b/pandas/core/arrays/sparse/dtype.py
index a9090570e64a9..8d17ed412f6b4 100644
--- a/pandas/core/arrays/sparse/dtype.py
+++ b/pandas/core/arrays/sparse/dtype.py
@@ -13,6 +13,7 @@
 from pandas.core.dtypes.cast import astype_nansafe
 from pandas.core.dtypes.common import (
     is_bool_dtype,
+    is_extension_array_dtype,
     is_object_dtype,
     is_scalar,
     is_string_dtype,
@@ -322,6 +323,9 @@ def update_dtype(self, dtype):
         dtype = pandas_dtype(dtype)
 
         if not isinstance(dtype, cls):
+            if is_extension_array_dtype(dtype):
+                raise TypeError("sparse arrays of extension dtypes not supported")
+
             fill_value = astype_nansafe(np.array(self.fill_value), dtype).item()
             dtype = cls(dtype, fill_value=fill_value)
 
diff --git a/pandas/core/arrays/string_.py b/pandas/core/arrays/string_.py
index 537b1cf3dd439..ac501a8afbe09 100644
--- a/pandas/core/arrays/string_.py
+++ b/pandas/core/arrays/string_.py
@@ -152,15 +152,21 @@ class StringArray(PandasArray):
     ['This is', 'some text', <NA>, 'data.']
     Length: 4, dtype: string
 
-    Unlike ``object`` dtype arrays, ``StringArray`` doesn't allow non-string
-    values.
+    Unlike arrays instantiated with ``dtype="object"``, ``StringArray``
+    will convert the values to strings.
 
+    >>> pd.array(['1', 1], dtype="object")
+    <PandasArray>
+    ['1', 1]
+    Length: 2, dtype: object
     >>> pd.array(['1', 1], dtype="string")
-    Traceback (most recent call last):
-    ...
-    ValueError: StringArray requires an object-dtype ndarray of strings.
+    <StringArray>
+    ['1', '1']
+    Length: 2, dtype: string
+
+    However, instantiating StringArrays directly with non-strings will raise an error.
 
-    For comparison methods, this returns a :class:`pandas.BooleanArray`
+    For comparison methods, `StringArray` returns a :class:`pandas.BooleanArray`:
 
     >>> pd.array(["a", None, "c"], dtype="string") == "a"
     <BooleanArray>
@@ -203,10 +209,15 @@ def _from_sequence(cls, scalars, dtype=None, copy=False):
         # TODO: it would be nice to do this in _validate / lib.is_string_array
         # We are already doing a scan over the values there.
         na_values = isna(result)
-        if na_values.any():
-            if result is scalars:
-                # force a copy now, if we haven't already
-                result = result.copy()
+        has_nans = na_values.any()
+        if has_nans and result is scalars:
+            # force a copy now, if we haven't already
+            result = result.copy()
+
+        # convert to str, then to object to avoid dtype like '<U3', then insert na_value
+        result = np.asarray(result, dtype=str)
+        result = np.asarray(result, dtype="object")
+        if has_nans:
             result[na_values] = StringDtype.na_value
 
         return cls(result)
diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py
index 424eb9d673df5..2a47a03b8d387 100644
--- a/pandas/core/dtypes/cast.py
+++ b/pandas/core/dtypes/cast.py
@@ -337,9 +337,16 @@ def maybe_cast_to_extension_array(cls: Type["ExtensionArray"], obj, dtype=None):
     -------
     ExtensionArray or obj
     """
+    from pandas.core.arrays.string_ import StringArray
+
     assert isinstance(cls, type), f"must pass a type: {cls}"
     assertion_msg = f"must pass a subclass of ExtensionArray: {cls}"
     assert issubclass(cls, ABCExtensionArray), assertion_msg
+
+    # Everything can be be converted to StringArrays, but we may not want to convert
+    if issubclass(cls, StringArray) and lib.infer_dtype(obj) != "string":
+        return obj
+
     try:
         result = cls._from_sequence(obj, dtype=dtype)
     except Exception:
diff --git a/pandas/tests/extension/base/casting.py b/pandas/tests/extension/base/casting.py
index f33f960e8e341..567a62a8b33a5 100644
--- a/pandas/tests/extension/base/casting.py
+++ b/pandas/tests/extension/base/casting.py
@@ -33,7 +33,13 @@ def test_tolist(self, data):
 
     def test_astype_str(self, data):
         result = pd.Series(data[:5]).astype(str)
-        expected = pd.Series(data[:5].astype(str))
+        expected = pd.Series([str(x) for x in data[:5]], dtype=str)
+        self.assert_series_equal(result, expected)
+
+    def test_astype_string(self, data):
+        # GH-33465
+        result = pd.Series(data[:5]).astype("string")
+        expected = pd.Series([str(x) for x in data[:5]], dtype="string")
         self.assert_series_equal(result, expected)
 
     def test_to_numpy(self, data):
diff --git a/pandas/tests/extension/decimal/array.py b/pandas/tests/extension/decimal/array.py
index 85d8ad6ec6e38..4d5be75ff8200 100644
--- a/pandas/tests/extension/decimal/array.py
+++ b/pandas/tests/extension/decimal/array.py
@@ -7,6 +7,7 @@
 import numpy as np
 
 from pandas.core.dtypes.base import ExtensionDtype
+from pandas.core.dtypes.common import pandas_dtype
 
 import pandas as pd
 from pandas.api.extensions import no_default, register_extension_dtype
@@ -130,9 +131,11 @@ def copy(self):
         return type(self)(self._data.copy())
 
     def astype(self, dtype, copy=True):
+        dtype = pandas_dtype(dtype)
         if isinstance(dtype, type(self.dtype)):
             return type(self)(self._data, context=dtype.context)
-        return np.asarray(self, dtype=dtype)
+
+        return super().astype(dtype, copy=copy)
 
     def __setitem__(self, key, value):
         if pd.api.types.is_list_like(value):
diff --git a/pandas/tests/extension/json/array.py b/pandas/tests/extension/json/array.py
index 94f971938b690..3132b39a7d6d6 100644
--- a/pandas/tests/extension/json/array.py
+++ b/pandas/tests/extension/json/array.py
@@ -21,6 +21,8 @@
 
 import numpy as np
 
+from pandas.core.dtypes.common import pandas_dtype
+
 import pandas as pd
 from pandas.api.extensions import ExtensionArray, ExtensionDtype
 
@@ -160,12 +162,18 @@ def astype(self, dtype, copy=True):
         # NumPy has issues when all the dicts are the same length.
         # np.array([UserDict(...), UserDict(...)]) fails,
         # but np.array([{...}, {...}]) works, so cast.
+        from pandas.core.arrays.string_ import StringDtype
 
+        dtype = pandas_dtype(dtype)
         # needed to add this check for the Series constructor
         if isinstance(dtype, type(self.dtype)) and dtype == self.dtype:
             if copy:
                 return self.copy()
             return self
+        elif isinstance(dtype, StringDtype):
+            value = self.astype(str)  # numpy doesn'y like nested dicts
+            return dtype.construct_array_type()._from_sequence(value, copy=False)
+
         return np.array([dict(x) for x in self], dtype=dtype, copy=copy)
 
     def unique(self):
diff --git a/pandas/tests/extension/test_numpy.py b/pandas/tests/extension/test_numpy.py
index 1e21249988df6..78000c0252375 100644
--- a/pandas/tests/extension/test_numpy.py
+++ b/pandas/tests/extension/test_numpy.py
@@ -139,6 +139,12 @@ def test_astype_str(self, data):
         # ValueError: setting an array element with a sequence
         super().test_astype_str(data)
 
+    @skip_nested
+    def test_astype_string(self, data):
+        # GH-33465
+        # ValueError: setting an array element with a sequence
+        super().test_astype_string(data)
+
 
 class TestConstructors(BaseNumPyTests, base.BaseConstructorsTests):
     @pytest.mark.skip(reason="We don't register our dtype")
diff --git a/pandas/tests/extension/test_sparse.py b/pandas/tests/extension/test_sparse.py
index e59b3f0600867..332a7f5a4e124 100644
--- a/pandas/tests/extension/test_sparse.py
+++ b/pandas/tests/extension/test_sparse.py
@@ -343,6 +343,16 @@ def test_astype_object_frame(self, all_data):
         # comp = result.dtypes.equals(df.dtypes)
         # assert not comp.any()
 
+    def test_astype_str(self, data):
+        result = pd.Series(data[:5]).astype(str)
+        expected_dtype = pd.SparseDtype(str, str(data.fill_value))
+        expected = pd.Series([str(x) for x in data[:5]], dtype=expected_dtype)
+        self.assert_series_equal(result, expected)
+
+    @pytest.mark.xfail(raises=TypeError, reason="no sparse StringDtype")
+    def test_astype_string(self, data):
+        super().test_astype_string(data)
+
 
 class TestArithmeticOps(BaseSparseTests, base.BaseArithmeticOpsTests):
     series_scalar_exc = None


Code review comments:
diff_hunk: @@ -63,6 +63,27 @@ Or ``astype`` after the ``Series`` or ``DataFrame`` is created
    s
    s.astype("string")
 
+
+.. versionchanged:: 1.1.0
+
+You can also use ``string`` dtype on non-string data and it will be converted to
+``string`` dtype:
+
+.. ipython:: python
+
+   s = pd.Series(['a', 2, np.nan], dtype="string")
+   s
+   type(s[1])
+
+or convert from existing pandas data:
+
+   s1 = pd.Series([1,2, np.nan], dtype="Int64")
path: doc/source/user_guide/text.rst
text: @user1:
this won't render (needs an ipython block here as well)

@author:
Fixed.