Repository: pandas-dev/pandas
Pull request: DEPR: Deprecate Series.view
- [ ] closes #20251 (Replace xxxx with the GitHub issue number)
- [ ] [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.


Problem statement:
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.

Hints from issue 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.

Commit message: Remove

Patch to review:
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index b0f51751b027c..cc777972ead0d 100644
--- a/doc/source/whatsnew/v2.2.0.rst
+++ b/doc/source/whatsnew/v2.2.0.rst
@@ -270,6 +270,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` (:issue:`20251`)
 - 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-keyword arguments in :meth:`DataFrame.to_clipboard`. (:issue:`54229`)
 - Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_csv` except ``path_or_buf``. (:issue:`54229`)
diff --git a/pandas/core/series.py b/pandas/core/series.py
index 1bbd10429ea22..c7ebf0f43df88 100644
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -938,6 +938,11 @@ def view(self, dtype: Dtype | None = None) -> Series:
         4      2
         dtype: int8
         """
+        warnings.warn(
+            "Series.view is deprecated and will be removed in a future version.",
+            FutureWarning,
+            stacklevel=find_stack_level(),
+        )
         # 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":
diff --git a/pandas/tests/copy_view/test_methods.py b/pandas/tests/copy_view/test_methods.py
index 61569a49863cb..4ed493bc04633 100644
--- a/pandas/tests/copy_view/test_methods.py
+++ b/pandas/tests/copy_view/test_methods.py
@@ -1922,7 +1922,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..2bfb06c53ebc2 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]._values.view("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 9ac20774b8c93..9926da27fbae7 100644
--- a/pandas/tests/io/test_sql.py
+++ b/pandas/tests/io/test_sql.py
@@ -1527,7 +1527,7 @@ def test_api_timedelta(conn, request):
         result_count = df.to_sql(name="test_timedelta", con=conn)
     assert result_count == 2
     result = sql.read_sql_query("SELECT * FROM test_timedelta", conn)
-    tm.assert_series_equal(result["foo"], df["foo"].view("int64"))
+    tm.assert_series_equal(result["foo"], df["foo"].astype("int64"))
 
 
 @pytest.mark.parametrize("conn", all_connectable)
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 7ab46154785ab..157206f912438 100644
--- a/pandas/tests/series/test_constructors.py
+++ b/pandas/tests/series/test_constructors.py
@@ -966,7 +966,8 @@ 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]")
+        with tm.assert_produces_warning(FutureWarning, match="view is deprecated"):
+            result = Series(Series(dates).view(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 97119127b1665..501d0bfbc4a1e 100644
--- a/pandas/tests/test_algos.py
+++ b/pandas/tests/test_algos.py
@@ -47,6 +47,10 @@
 )
 import pandas.core.common as com
 
+pytestmark = pytest.mark.filterwarnings(
+    "ignore:Series.view is deprecated and will be removed in a future version.:FutureWarning"  # noqa: E501
+)
+
 
 class TestFactorize:
     def test_factorize_complex(self):
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
 


Code review comments:
diff_hunk: @@ -966,7 +966,8 @@ 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]")
+        with tm.assert_produces_warning(FutureWarning, match="view is deprecated"):
+            result = Series(Series(dates).view(np.int64) / 1000000, dtype="M8[ms]")
path: pandas/tests/series/test_constructors.py
text: @user1:
```suggestion
        result = Series(Series(dates).astype(np.int64) / 1000000, dtype="M8[ms]")
```

(this test is not about `view()` but about the constructor from ints, so I think we can already update it and then we don't have to change it later again)

diff_hunk: @@ -938,6 +938,11 @@ def view(self, dtype: Dtype | None = None) -> Series:
         4      2
         dtype: int8
         """
+        warnings.warn(
+            "Series.view is deprecated and will be removed in a future version.",
path: pandas/core/series.py
text: @user1:
Point to astype instead if the goal was to change the dtype?

@author:
Yeah

diff_hunk: @@ -47,6 +47,10 @@
 )
 import pandas.core.common as com
 
+pytestmark = pytest.mark.filterwarnings(
+    "ignore:Series.view is deprecated and will be removed in a future version.:FutureWarning"  # noqa: E501
path: pandas/tests/test_algos.py
text: @user1:
This seems a bit a "dangerous" ignore, because we want to be sure the algos itself aren't raising the warning, and algos seems a part of the codebase that is using `view()` regularly (although already not with Series objects apparently, assuming it is correct no change is needed there)

@author:
I can ignore them specifically, will update later

diff_hunk: @@ -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]._values.view("M8[ns]")
path: pandas/tests/groupby/test_timegrouper.py
text: @user1:
```suggestion
        df[1] = df[1].astype("M8[ns]")
```

(I don't think there is a reason for the view here?)