Repository: pandas-dev/pandas

Issue:
DEPR: deprecate DataFrame.last and Series.last
just opening this so it's not forgotten - xref https://github.com/pandas-dev/pandas/pull/53419


Discussion:
take
@MarcoGorelli I'll take this one unless you're already working on it. I see that you're involved with the PR that you referenced
Go ahead thanks! 

Patch:
diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst
index 44691e4265f5b..313f2fc728eab 100644
--- a/doc/source/whatsnew/v2.1.0.rst
+++ b/doc/source/whatsnew/v2.1.0.rst
@@ -288,6 +288,7 @@ Deprecations
 - Deprecated :func:`value_counts`, use ``pd.Series(obj).value_counts()`` instead (:issue:`47862`)
 - Deprecated :meth:`Series.first` and :meth:`DataFrame.first` (please create a mask and filter using ``.loc`` instead) (:issue:`45908`)
 - Deprecated :meth:`Series.interpolate` and :meth:`DataFrame.interpolate` for object-dtype (:issue:`53631`)
+- Deprecated :meth:`Series.last` and :meth:`DataFrame.last` (please create a mask and filter using ``.loc`` instead) (:issue:`53692`)
 - Deprecated allowing ``downcast`` keyword other than ``None``, ``False``, "infer", or a dict with these as values in :meth:`Series.fillna`, :meth:`DataFrame.fillna` (:issue:`40988`)
 - Deprecated allowing arbitrary ``fill_value`` in :class:`SparseDtype`, in a future version the ``fill_value`` will need to be compatible with the ``dtype.subtype``, either a scalar that can be held by that subtype or ``NaN`` for integer or bool subtypes (:issue:`23124`)
 - Deprecated behavior of :func:`assert_series_equal` and :func:`assert_frame_equal` considering NA-like values (e.g. ``NaN`` vs ``None`` as equivalent) (:issue:`52081`)
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
index f635cc28ed4fa..5145a929247bd 100644
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -9321,6 +9321,11 @@ def last(self, offset) -> Self:
         at_time : Select values at a particular time of the day.
         between_time : Select values between particular times of the day.
 
+        Notes
+        -----
+        .. deprecated:: 2.1.0
+            Please create a mask and filter using `.loc` instead
+
         Examples
         --------
         >>> i = pd.date_range('2018-04-09', periods=4, freq='2D')
@@ -9334,7 +9339,7 @@ def last(self, offset) -> Self:
 
         Get the rows for the last 3 days:
 
-        >>> ts.last('3D')
+        >>> ts.last('3D') # doctest: +SKIP
                     A
         2018-04-13  3
         2018-04-15  4
@@ -9343,6 +9348,13 @@ def last(self, offset) -> Self:
         3 observed days in the dataset, and therefore data for 2018-04-11 was
         not returned.
         """
+        warnings.warn(
+            "last is deprecated and will be removed in a future version. "
+            "Please create a mask and filter using `.loc` instead",
+            FutureWarning,
+            stacklevel=find_stack_level(),
+        )
+
         if not isinstance(self.index, DatetimeIndex):
             raise TypeError("'last' only supports a DatetimeIndex index")
 


Test patch:
diff --git a/pandas/tests/frame/methods/test_first_and_last.py b/pandas/tests/frame/methods/test_first_and_last.py
index 18173f7c66198..2e85edc7ed0ea 100644
--- a/pandas/tests/frame/methods/test_first_and_last.py
+++ b/pandas/tests/frame/methods/test_first_and_last.py
@@ -11,6 +11,7 @@
 import pandas._testing as tm
 
 deprecated_msg = "first is deprecated"
+last_deprecated_msg = "last is deprecated"
 
 
 class TestFirst:
@@ -55,29 +56,38 @@ def test_first_last_raises(self, frame_or_series):
             obj.first("1D")
 
         msg = "'last' only supports a DatetimeIndex index"
-        with pytest.raises(TypeError, match=msg):  # index is not a DatetimeIndex
+        with tm.assert_produces_warning(
+            FutureWarning, match=last_deprecated_msg
+        ), pytest.raises(
+            TypeError, match=msg
+        ):  # index is not a DatetimeIndex
             obj.last("1D")
 
     def test_last_subset(self, frame_or_series):
         ts = tm.makeTimeDataFrame(freq="12h")
         ts = tm.get_obj(ts, frame_or_series)
-        result = ts.last("10d")
+        with tm.assert_produces_warning(FutureWarning, match=last_deprecated_msg):
+            result = ts.last("10d")
         assert len(result) == 20
 
         ts = tm.makeTimeDataFrame(nper=30, freq="D")
         ts = tm.get_obj(ts, frame_or_series)
-        result = ts.last("10d")
+        with tm.assert_produces_warning(FutureWarning, match=last_deprecated_msg):
+            result = ts.last("10d")
         assert len(result) == 10
 
-        result = ts.last("21D")
+        with tm.assert_produces_warning(FutureWarning, match=last_deprecated_msg):
+            result = ts.last("21D")
         expected = ts["2000-01-10":]
         tm.assert_equal(result, expected)
 
-        result = ts.last("21D")
+        with tm.assert_produces_warning(FutureWarning, match=last_deprecated_msg):
+            result = ts.last("21D")
         expected = ts[-21:]
         tm.assert_equal(result, expected)
 
-        result = ts[:0].last("3M")
+        with tm.assert_produces_warning(FutureWarning, match=last_deprecated_msg):
+            result = ts[:0].last("3M")
         tm.assert_equal(result, ts[:0])
 
     @pytest.mark.parametrize("start, periods", [("2010-03-31", 1), ("2010-03-30", 2)])
@@ -104,7 +114,8 @@ def test_first_with_first_day_end_of_frq_n_greater_one(self, frame_or_series):
     def test_empty_not_input(self):
         # GH#51032
         df = DataFrame(index=pd.DatetimeIndex([]))
-        result = df.last(offset=1)
+        with tm.assert_produces_warning(FutureWarning, match=last_deprecated_msg):
+            result = df.last(offset=1)
 
         with tm.assert_produces_warning(FutureWarning, match=deprecated_msg):
             result = df.first(offset=1)
diff --git a/pandas/tests/generic/test_finalize.py b/pandas/tests/generic/test_finalize.py
index 22460b1ea9dfe..f827eaf63a342 100644
--- a/pandas/tests/generic/test_finalize.py
+++ b/pandas/tests/generic/test_finalize.py
@@ -395,7 +395,8 @@ def ndframe_method(request):
 
 
 @pytest.mark.filterwarnings(
-    "ignore:DataFrame.fillna with 'method' is deprecated:FutureWarning"
+    "ignore:DataFrame.fillna with 'method' is deprecated:FutureWarning",
+    "ignore:last is deprecated:FutureWarning",
 )
 def test_finalize_called(ndframe_method):
     cls, init_args, method = ndframe_method
@@ -423,6 +424,23 @@ def test_finalize_first(data):
         assert result.attrs == {"a": 1}
 
 
+@pytest.mark.parametrize(
+    "data",
+    [
+        pd.Series(1, pd.date_range("2000", periods=4)),
+        pd.DataFrame({"A": [1, 1, 1, 1]}, pd.date_range("2000", periods=4)),
+    ],
+)
+def test_finalize_last(data):
+    # GH 53710
+    deprecated_msg = "last is deprecated"
+
+    data.attrs = {"a": 1}
+    with tm.assert_produces_warning(FutureWarning, match=deprecated_msg):
+        result = data.last("3D")
+        assert result.attrs == {"a": 1}
+
+
 @not_implemented_mark
 def test_finalize_called_eval_numexpr():
     pytest.importorskip("numexpr")