Repository: pandas-dev/pandas

Issue:
DEPR: Series.view
we do not fully control memory in pandas currently; its defered to numpy, so a ``view()`` rarely makes sense, esp since we will change the dtype appropriate upon setting, breaking the view association.

```
In [35]: s = pd.Series([1, 2, 3])

In [36]: s_view = s.view()

In [39]: s_view
Out[39]: 
0    4.940656e-324
1    9.881313e-324
2    1.482197e-323
dtype: float64
```

we should just remove ``.view()`` entirely.


Discussion:
Didn't see this one yet, but just opened: https://github.com/pandas-dev/pandas/issues/20281. 
Deprecating is also a solution, but just fixing the behaviour when no dtype is specified (the one you show above) is also an option.

But I agree it is hardly useful given the constraint on keeping the same number of elements.
The only meaningful use case I could think of is the one I chose for the example in the docstring: converting unsigned int from/to signed int. Basically every other byte conversion is very unlikely to be useful for regular pandas users.

Contrived scenario:

Let's say I have a really big Series of `int8` integers that barely fits in memory (which is basically why I'm trying to use `int8` instead of `int64`). I then realize that actually `int8` isn't enough because I have some data points in the 127~255 range, and because none of the data points are negative I could have been using `uint8` all along. Creating a `uint8` view would be my salvation - assuming that for some reason I don't want to simply redo the computation steps this time using `uint8` from the beginning.
The original example has been fixed since

```
In [4]: In [35]: s = pd.Series([1, 2, 3])
   ...:
   ...: In [36]: s_view = s.view()

In [5]: s_view
Out[5]:
0    1
1    2
2    3
dtype: int64
```

And `view` is in the ExtensionArray interface as well so it would need deprecating there as well. Not sure how pervasively this is used internally as well cc @jbrockmendel 
We use .view a lot in the dt64/td64/period code.  i dont think we use it often in the general EA case outside of (Series|Index).view
I would like to bring this up again, at least for the `Series` method. It's a method on our core object's precious namespace, but I don't think there is an actual use case for users where you _need_ this (apart from the example of int8 -> uint8 given above in https://github.com/pandas-dev/pandas/issues/20251#issuecomment-372131876, but I think that's a very advanced use case that can be worked around in other ways as well). 
If you want to avoid the copy for the subset of casts where it is not needed, users can always do `.astype(.., copy=False)` (which will actually become the default with CoW enabled). 

Deprecating / removing it for `Series`, doesn't mean that we need to do the same for the EAs, if that is something we use ourselves a lot. 
I think it's fine to have this method on the array (np.ndarray has it as well), but not on Series (I _think_ one of the reasons we have this method on Series in the first place is that at some point in time Series was an ndarray subclass, and so inherited this from np.ndarray)
No objection to deprecating it on Series.

Patch:
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index 8893fe0ecd398..d7ee47dc82d89 100644
--- a/doc/source/whatsnew/v2.2.0.rst
+++ b/doc/source/whatsnew/v2.2.0.rst
@@ -362,6 +362,7 @@ Other Deprecations
 - Deprecated :func:`read_gbq` and :meth:`DataFrame.to_gbq`. Use ``pandas_gbq.read_gbq`` and ``pandas_gbq.to_gbq`` instead https://pandas-gbq.readthedocs.io/en/latest/api.html (:issue:`55525`)
 - Deprecated :meth:`.DataFrameGroupBy.fillna` and :meth:`.SeriesGroupBy.fillna`; use :meth:`.DataFrameGroupBy.ffill`, :meth:`.DataFrameGroupBy.bfill` for forward and backward filling or :meth:`.DataFrame.fillna` to fill with a single value (or the Series equivalents) (:issue:`55718`)
 - Deprecated :meth:`Index.format`, use ``index.astype(str)`` or ``index.map(formatter)`` instead (:issue:`55413`)
+- Deprecated :meth:`Series.view`, use ``astype`` instead to change the dtype (:issue:`20251`)
 - Deprecated ``core.internals`` members ``Block``, ``ExtensionBlock``, and ``DatetimeTZBlock``, use public APIs instead (:issue:`55139`)
 - Deprecated ``year``, ``month``, ``quarter``, ``day``, ``hour``, ``minute``, and ``second`` keywords in the :class:`PeriodIndex` constructor, use :meth:`PeriodIndex.from_fields` instead (:issue:`55960`)
 - Deprecated allowing non-integer ``periods`` argument in :func:`date_range`, :func:`timedelta_range`, :func:`period_range`, and :func:`interval_range` (:issue:`56036`)
diff --git a/pandas/core/series.py b/pandas/core/series.py
index a9679f22f9933..6b06786549ade 100644
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -877,6 +877,10 @@ def view(self, dtype: Dtype | None = None) -> Series:
         """
         Create a new view of the Series.
 
+        .. deprecated:: 2.2.0
+            ``Series.view`` is deprecated and will be removed in a future version.
+            Use :meth:`Series.astype` as an alternative to change the dtype.
+
         This function will return a new Series with a view of the same
         underlying values in memory, optionally reinterpreted with a new data
         type. The new data type must preserve the same size in bytes as to not
@@ -907,38 +911,14 @@ def view(self, dtype: Dtype | None = None) -> Series:
 
         Examples
         --------
-        >>> s = pd.Series([-2, -1, 0, 1, 2], dtype='int8')
-        >>> s
-        0   -2
-        1   -1
-        2    0
-        3    1
-        4    2
-        dtype: int8
-
-        The 8 bit signed integer representation of `-1` is `0b11111111`, but
-        the same bytes represent 255 if read as an 8 bit unsigned integer:
-
-        >>> us = s.view('uint8')
-        >>> us
-        0    254
-        1    255
-        2      0
-        3      1
-        4      2
-        dtype: uint8
-
-        The views share the same underlying values:
-
-        >>> us[0] = 128
-        >>> s
-        0   -128
-        1     -1
-        2      0
-        3      1
-        4      2
-        dtype: int8
+        Use ``astype`` to change the dtype instead.
         """
+        warnings.warn(
+            "Series.view is deprecated and will be removed in a future version. "
+            "Use ``astype`` as an alternative to change the dtype.",
+            FutureWarning,
+            stacklevel=2,
+        )
         # self.array instead of self._values so we piggyback on NumpyExtensionArray
         #  implementation
         res_values = self.array.view(dtype)
diff --git a/pandas/core/window/ewm.py b/pandas/core/window/ewm.py
index 82b4746aa57a5..db659713c6f16 100644
--- a/pandas/core/window/ewm.py
+++ b/pandas/core/window/ewm.py
@@ -15,6 +15,7 @@
     is_datetime64_ns_dtype,
     is_numeric_dtype,
 )
+from pandas.core.dtypes.generic import ABCSeries
 from pandas.core.dtypes.missing import isna
 
 from pandas.core import common
@@ -118,6 +119,8 @@ def _calculate_deltas(
     np.ndarray
         Diff of the times divided by the half-life
     """
+    if isinstance(times, ABCSeries):
+        times = times._values
     _times = np.asarray(times.view(np.int64), dtype=np.float64)
     # TODO: generalize to non-nano?
     _halflife = float(Timedelta(halflife).as_unit("ns")._value)
diff --git a/pandas/io/stata.py b/pandas/io/stata.py
index 70294e8a62cca..1eb8f531dc62a 100644
--- a/pandas/io/stata.py
+++ b/pandas/io/stata.py
@@ -429,9 +429,9 @@ def parse_dates_safe(
                 d["year"] = date_index._data.year
                 d["month"] = date_index._data.month
             if days:
-                days_in_ns = dates.view(np.int64) - to_datetime(
+                days_in_ns = dates._values.view(np.int64) - to_datetime(
                     d["year"], format="%Y"
-                ).view(np.int64)
+                )._values.view(np.int64)
                 d["days"] = days_in_ns // NS_PER_DAY
 
         elif infer_dtype(dates, skipna=False) == "datetime":


Test patch:
diff --git a/pandas/tests/copy_view/test_methods.py b/pandas/tests/copy_view/test_methods.py
index 6416dd50daddc..3fd86f916c771 100644
--- a/pandas/tests/copy_view/test_methods.py
+++ b/pandas/tests/copy_view/test_methods.py
@@ -1934,7 +1934,8 @@ def test_series_view(using_copy_on_write, warn_copy_on_write):
     ser = Series([1, 2, 3])
     ser_orig = ser.copy()
 
-    ser2 = ser.view()
+    with tm.assert_produces_warning(FutureWarning, match="is deprecated"):
+        ser2 = ser.view()
     assert np.shares_memory(get_array(ser), get_array(ser2))
     if using_copy_on_write:
         assert not ser2._mgr._has_no_reference(0)
diff --git a/pandas/tests/frame/indexing/test_where.py b/pandas/tests/frame/indexing/test_where.py
index 4576a86ad27cd..6b2bf211ab748 100644
--- a/pandas/tests/frame/indexing/test_where.py
+++ b/pandas/tests/frame/indexing/test_where.py
@@ -771,7 +771,8 @@ def test_where_datetimelike_noop(self, dtype):
         # GH#45135, analogue to GH#44181 for Period don't raise on no-op
         # For td64/dt64/dt64tz we already don't raise, but also are
         #  checking that we don't unnecessarily upcast to object.
-        ser = Series(np.arange(3) * 10**9, dtype=np.int64).view(dtype)
+        with tm.assert_produces_warning(FutureWarning, match="is deprecated"):
+            ser = Series(np.arange(3) * 10**9, dtype=np.int64).view(dtype)
         df = ser.to_frame()
         mask = np.array([False, False, False])
 
diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py
index bf17b61b0e3f3..ee9255b732a86 100644
--- a/pandas/tests/frame/test_constructors.py
+++ b/pandas/tests/frame/test_constructors.py
@@ -1101,8 +1101,8 @@ def test_constructor_maskedarray_nonfloat(self):
         mat2[0, 0] = 1
         mat2[1, 2] = 2
         frame = DataFrame(mat2, columns=["A", "B", "C"], index=[1, 2])
-        assert 1 == frame["A"].view("i8")[1]
-        assert 2 == frame["C"].view("i8")[2]
+        assert 1 == frame["A"].astype("i8")[1]
+        assert 2 == frame["C"].astype("i8")[2]
 
         # masked bool promoted to object
         mat = ma.masked_all((2, 3), dtype=bool)
diff --git a/pandas/tests/generic/test_finalize.py b/pandas/tests/generic/test_finalize.py
index 68746b9e9a803..866e9e203ffe3 100644
--- a/pandas/tests/generic/test_finalize.py
+++ b/pandas/tests/generic/test_finalize.py
@@ -31,11 +31,6 @@
 # - Callable: pass the constructed value with attrs set to this.
 
 _all_methods = [
-    (
-        pd.Series,
-        (np.array([0], dtype="float64")),
-        operator.methodcaller("view", "int64"),
-    ),
     (pd.Series, ([0],), operator.methodcaller("take", [])),
     (pd.Series, ([0],), operator.methodcaller("__getitem__", [True])),
     (pd.Series, ([0],), operator.methodcaller("repeat", 2)),
diff --git a/pandas/tests/groupby/test_cumulative.py b/pandas/tests/groupby/test_cumulative.py
index 25534865b3486..bf572609f3d37 100644
--- a/pandas/tests/groupby/test_cumulative.py
+++ b/pandas/tests/groupby/test_cumulative.py
@@ -216,7 +216,7 @@ def test_cummax_i8_at_implementation_bound():
     # the minimum value used to be treated as NPY_NAT+1 instead of NPY_NAT
     #  for int64 dtype GH#46382
     ser = Series([pd.NaT._value + n for n in range(5)])
-    df = DataFrame({"A": 1, "B": ser, "C": ser.view("M8[ns]")})
+    df = DataFrame({"A": 1, "B": ser, "C": ser._values.view("M8[ns]")})
     gb = df.groupby("A")
 
     res = gb.cummax()
diff --git a/pandas/tests/groupby/test_timegrouper.py b/pandas/tests/groupby/test_timegrouper.py
index be02c7f79ba01..d8b00b9b9a483 100644
--- a/pandas/tests/groupby/test_timegrouper.py
+++ b/pandas/tests/groupby/test_timegrouper.py
@@ -717,7 +717,7 @@ def test_groupby_groups_periods(self):
 
     def test_groupby_first_datetime64(self):
         df = DataFrame([(1, 1351036800000000000), (2, 1351036800000000000)])
-        df[1] = df[1].view("M8[ns]")
+        df[1] = df[1].astype("M8[ns]")
 
         assert issubclass(df[1].dtype.type, np.datetime64)
 
diff --git a/pandas/tests/io/json/test_pandas.py b/pandas/tests/io/json/test_pandas.py
index 7312facc44c26..411cc90ba41a7 100644
--- a/pandas/tests/io/json/test_pandas.py
+++ b/pandas/tests/io/json/test_pandas.py
@@ -168,7 +168,7 @@ def test_frame_non_unique_columns(self, orient, data):
                 # in milliseconds; these are internally stored in nanosecond,
                 # so divide to get where we need
                 # TODO: a to_epoch method would also solve; see GH 14772
-                expected.iloc[:, 0] = expected.iloc[:, 0].view(np.int64) // 1000000
+                expected.iloc[:, 0] = expected.iloc[:, 0].astype(np.int64) // 1000000
         elif orient == "split":
             expected = df
             expected.columns = ["x", "x.1"]
diff --git a/pandas/tests/io/test_sql.py b/pandas/tests/io/test_sql.py
index 45d0a839b9e1a..d7c69ff17749c 100644
--- a/pandas/tests/io/test_sql.py
+++ b/pandas/tests/io/test_sql.py
@@ -1908,7 +1908,7 @@ def test_api_timedelta(conn, request):
             name="foo",
         )
     else:
-        expected = df["foo"].view("int64")
+        expected = df["foo"].astype("int64")
     tm.assert_series_equal(result["foo"], expected)
 
 
diff --git a/pandas/tests/series/methods/test_view.py b/pandas/tests/series/methods/test_view.py
index a9b29c9329193..7e0ac372cd443 100644
--- a/pandas/tests/series/methods/test_view.py
+++ b/pandas/tests/series/methods/test_view.py
@@ -9,6 +9,10 @@
 )
 import pandas._testing as tm
 
+pytestmark = pytest.mark.filterwarnings(
+    "ignore:Series.view is deprecated and will be removed in a future version.:FutureWarning"  # noqa: E501
+)
+
 
 class TestView:
     def test_view_i8_to_datetimelike(self):
diff --git a/pandas/tests/series/test_constructors.py b/pandas/tests/series/test_constructors.py
index 84c612c43da29..4281610b28951 100644
--- a/pandas/tests/series/test_constructors.py
+++ b/pandas/tests/series/test_constructors.py
@@ -969,7 +969,7 @@ def test_constructor_dtype_datetime64_10(self):
         # GH3414 related
         expected = Series(pydates, dtype="datetime64[ms]")
 
-        result = Series(Series(dates).view(np.int64) / 1000000, dtype="M8[ms]")
+        result = Series(Series(dates).astype(np.int64) / 1000000, dtype="M8[ms]")
         tm.assert_series_equal(result, expected)
 
         result = Series(dates, dtype="datetime64[ms]")
diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py
index a6e63dfd5f409..1fd92af38ac1a 100644
--- a/pandas/tests/test_algos.py
+++ b/pandas/tests/test_algos.py
@@ -976,7 +976,7 @@ def test_isin_datetimelike_values_numeric_comps(self, dtype, dtype1):
         # Anything but object and we get all-False shortcut
 
         dta = date_range("2013-01-01", periods=3)._values
-        arr = Series(dta.view("i8")).view(dtype1)._values
+        arr = Series(dta.view("i8")).array.view(dtype1)
 
         comps = arr.view("i8").astype(dtype)
 
diff --git a/pandas/tests/test_nanops.py b/pandas/tests/test_nanops.py
index a0062d2b6dd44..632d9783c7f81 100644
--- a/pandas/tests/test_nanops.py
+++ b/pandas/tests/test_nanops.py
@@ -1114,12 +1114,16 @@ def test_nanmean(self, unit):
         expected = dti[1]
 
         for obj in [dti, DatetimeArray(dti), Series(dti)]:
+            if isinstance(obj, Series):
+                obj = obj._values
             result = nanops.nanmean(obj)
             assert result == expected
 
         dti2 = dti.insert(1, pd.NaT)
 
         for obj in [dti2, DatetimeArray(dti2), Series(dti2)]:
+            if isinstance(obj, Series):
+                obj = obj._values
             result = nanops.nanmean(obj)
             assert result == expected