Repository: pandas-dev/pandas

Issue:
DEPR: Deprecate downcast keyword for fillna
This came up recently in #40861 
Depending on the way forward this would allow us to get downcast out from Block or be stricter about it

Thougts?

cc @jbrockmendel @jreback 


Discussion:
The more I look at this, the wonkier it seems.  #40861 implemented a test case:

```
df = pd.DataFrame({"col1": [1, np.nan]})

df.fillna({"col1": 2}, downcast={"col1": "int64"})
```

But similar calls still raise:

```
>>> df.fillna({"col1": 2}, downcast={"col1": np.int64})
[...]
TypeError: <class 'numpy.int64'>

>>> df.fillna(2, downcast={"col1": "int64"})
[...]
AssertionError: dtypes as dict is not supported yet

>>> df.fillna(2, downcast="int64")
[...]
ValueError: downcast must have a dictionary or 'infer' as its argument
```

Moreover, there are several places in NDFrame.fillna and Block.fillna where we ignore the 'downcast' keyword so end up using None.  There's also a couple of layers of special-casing in Block._maybe_downcast and Block.downcast.

Notwithstanding the places where the downcast keyword is ignored, the status quo is roughly:

```
if downcast is None:
    if self.dtype == object:
        return datetime_centric_special_case()
    elif self.dtype.kind in ["f", "m", "M"]:
        return no_op()
    elif self.ndim == 1:
        downcast  = "infer"
        return do_inference()
    else:
        return no_op()

else:
    if self.dtype == object:
        return no_op()
    elif self.ndim == 1:
        return one_case_where_we_actually_respect_downcast_keyword()
    elif downcast != "infer":
          raise ValueErrorOrAssertionError
     else:
        return do_inference()
```

Some of this we can improve by actually respecting the keyword in more places and call that a bugfix.  But the rest of the special-casing (object dtype, ndim==1 vs ndim==2) will likely change things and need a deprecation.

AFAICT the useful cases for downcasting after fillna (and interpolate, the other places from which _maybe_downcast is called) are 1) float->int and 2) object->not_object.

If this were greenfield, my inclination would be to make `downcast` take a bool, with False meaning no downcasting and True meaning "infer". (we could also take a dict but it would get unpacked before making it to the Block methods)


xref #11537
More questionable behavior: 1) we do the downcast even if the fillna is a no-op, 2) we ignore `inplace=True` and downcast anyway.
Based on a few days of trying to figure this out, along with #11537, I propose we deprecate the 'downcast' keyword in 'fillna', 'interpolate', 'ffill', 'bfill', with the future behavior being to _always_ downcast for object dtype and never for anything else (the only relevant case for "anything else" being floats)
sounds ok to me 
Having some trouble with the "do X instead" part because we don't have a method for downcasting floats (do we?)
we don't downcast floats as a general rule
update: deprecating the argument seems increasingly invasive.  Leaning towards chipping away at the inconsistencies and calling them bugfixes.
@jbrockmendel - I removed the deprecate tag; do you know if this issue remain open for tracking or if it is now duplicative of other issues?
I've completely lost track
For the four examples in the top of https://github.com/pandas-dev/pandas/issues/40988#issuecomment-952310415, the first remains okay, the last is now fixed, and the other two still raise. A quick search for downcast and fillna gives some issues, but none of the nature here.
@phofl what are your current thoughts here?
Ideally, we would get rid of downcast in fillna and others and provide a proper down casting method?
Sounds good to me.  Want to take point on this?
Yep, can take care of this.

Would add deprecation note and new method for 2.1, so that we can immediately point users to an alternative.
I got annoyed enough by this to spend some more time on it.  Thinking of deprecating in steps:

1) Deprecate any "downcast" other than [None, False, "infer"] (or dict with each value being one of these)
2) Deprecate behavior of downcasting that casts round-floats to ints (once enforced this would allow us to reuse Block.convert instead of Block._downcast_2d)
3) Deprecate downcast keyword altogether

Patch:
diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst
index 517baa648d805..970db240a453a 100644
--- a/doc/source/whatsnew/v2.1.0.rst
+++ b/doc/source/whatsnew/v2.1.0.rst
@@ -255,6 +255,7 @@ Deprecations
 - Deprecated making the functions in a list of functions given to :meth:`DataFrame.agg` attempt to operate on each element in the :class:`DataFrame` and only operate on the columns of the :class:`DataFrame` if the elementwise operations failed. To keep the current behavior, use :meth:`DataFrame.transform` instead. (:issue:`53325`)
 - Deprecated passing a :class:`DataFrame` to :meth:`DataFrame.from_records`, use :meth:`DataFrame.set_index` or :meth:`DataFrame.drop` instead (:issue:`51353`)
 - Deprecated silently dropping unrecognized timezones when parsing strings to datetimes (:issue:`18702`)
+- Deprecated the "downcast" keyword in :meth:`Series.interpolate`, :meth:`DataFrame.interpolate`, :meth:`Series.fillna`, :meth:`DataFrame.fillna`, :meth:`Series.ffill`, :meth:`DataFrame.ffill`, :meth:`Series.bfill`, :meth:`DataFrame.bfill` (:issue:`40988`)
 - Deprecated the ``axis`` keyword in :meth:`DataFrame.ewm`, :meth:`Series.ewm`, :meth:`DataFrame.rolling`, :meth:`Series.rolling`, :meth:`DataFrame.expanding`, :meth:`Series.expanding` (:issue:`51778`)
 - Deprecated the ``axis`` keyword in :meth:`DataFrame.resample`, :meth:`Series.resample` (:issue:`51778`)
 - Deprecated the behavior of :func:`concat` with both ``len(keys) != len(objs)``, in a future version this will raise instead of truncating to the shorter of the two sequences (:issue:`43485`)
@@ -292,7 +293,6 @@ Deprecations
 - 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`)
 - Deprecated bytes input to :func:`read_excel`. To read a file path, use a string or path-like object. (:issue:`53767`)
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
index a4b4466e8d609..94853cf50eb55 100644
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -6870,25 +6870,21 @@ def convert_dtypes(
     # ----------------------------------------------------------------------
     # Filling NA's
 
-    def _deprecate_downcast(self, downcast) -> None:
-        if isinstance(downcast, dict):
-            # GH#40988
-            for dc in downcast.values():
-                if dc is not None and dc is not False and dc != "infer":
-                    warnings.warn(
-                        "downcast entries other than None, False, and 'infer' "
-                        "are deprecated and will raise in a future version",
-                        FutureWarning,
-                        stacklevel=find_stack_level(),
-                    )
-        elif downcast is not None and downcast is not False and downcast != "infer":
-            # GH#40988
+    def _deprecate_downcast(self, downcast, method_name: str):
+        # GH#40988
+        if downcast is not lib.no_default:
             warnings.warn(
-                "downcast other than None, False, and 'infer' are deprecated "
-                "and will raise in a future version",
+                f"The 'downcast' keyword in {method_name} is deprecated and "
+                "will be removed in a future version. Use "
+                "res.infer_objects(copy=False) to infer non-object dtype, or "
+                "pd.to_numeric with the 'downcast' keyword to downcast numeric "
+                "results.",
                 FutureWarning,
                 stacklevel=find_stack_level(),
             )
+        else:
+            downcast = None
+        return downcast
 
     @final
     def _pad_or_backfill(
@@ -6977,7 +6973,7 @@ def fillna(
         axis: Axis | None = None,
         inplace: bool_t = False,
         limit: int | None = None,
-        downcast: dict | None = None,
+        downcast: dict | None | lib.NoDefault = lib.no_default,
     ) -> Self | None:
         """
         Fill NA/NaN values using the specified method.
@@ -7096,7 +7092,8 @@ def fillna(
                 stacklevel=find_stack_level(),
             )
 
-        self._deprecate_downcast(downcast)
+        was_no_default = downcast is lib.no_default
+        downcast = self._deprecate_downcast(downcast, "fillna")
 
         # set the default here, so functions examining the signaure
         # can detect if something was set (e.g. in groupby) (GH9221)
@@ -7113,7 +7110,11 @@ def fillna(
                 axis=axis,
                 limit=limit,
                 inplace=inplace,
-                downcast=downcast,
+                # error: Argument "downcast" to "_fillna_with_method" of "NDFrame"
+                # has incompatible type "Union[Dict[Any, Any], None,
+                # Literal[_NoDefault.no_default]]"; expected
+                # "Optional[Dict[Any, Any]]"
+                downcast=downcast,  # type: ignore[arg-type]
             )
         else:
             if self.ndim == 1:
@@ -7157,13 +7158,20 @@ def fillna(
                     if k not in result:
                         continue
 
-                    # error: Item "None" of "Optional[Dict[Any, Any]]" has no
-                    # attribute "get"
-                    downcast_k = (
-                        downcast
-                        if not is_dict
-                        else downcast.get(k)  # type: ignore[union-attr]
-                    )
+                    if was_no_default:
+                        downcast_k = lib.no_default
+                    else:
+                        downcast_k = (
+                            # error: Incompatible types in assignment (expression
+                            # has type "Union[Dict[Any, Any], None,
+                            # Literal[_NoDefault.no_default], Any]", variable has
+                            # type "_NoDefault")
+                            downcast  # type: ignore[assignment]
+                            if not is_dict
+                            # error: Item "None" of "Optional[Dict[Any, Any]]" has
+                            # no attribute "get"
+                            else downcast.get(k)  # type: ignore[union-attr]
+                        )
 
                     res_k = result[k].fillna(v, limit=limit, downcast=downcast_k)
 
@@ -7236,7 +7244,7 @@ def ffill(
         axis: None | Axis = ...,
         inplace: Literal[False] = ...,
         limit: None | int = ...,
-        downcast: dict | None = ...,
+        downcast: dict | None | lib.NoDefault = ...,
     ) -> Self:
         ...
 
@@ -7247,7 +7255,7 @@ def ffill(
         axis: None | Axis = ...,
         inplace: Literal[True],
         limit: None | int = ...,
-        downcast: dict | None = ...,
+        downcast: dict | None | lib.NoDefault = ...,
     ) -> None:
         ...
 
@@ -7258,7 +7266,7 @@ def ffill(
         axis: None | Axis = ...,
         inplace: bool_t = ...,
         limit: None | int = ...,
-        downcast: dict | None = ...,
+        downcast: dict | None | lib.NoDefault = ...,
     ) -> Self | None:
         ...
 
@@ -7270,7 +7278,7 @@ def ffill(
         axis: None | Axis = None,
         inplace: bool_t = False,
         limit: None | int = None,
-        downcast: dict | None = None,
+        downcast: dict | None | lib.NoDefault = lib.no_default,
     ) -> Self | None:
         """
         Synonym for :meth:`DataFrame.fillna` with ``method='ffill'``.
@@ -7309,10 +7317,17 @@ def ffill(
         3   3.0
         dtype: float64
         """
-        self._deprecate_downcast(downcast)
+        downcast = self._deprecate_downcast(downcast, "ffill")
 
         return self._pad_or_backfill(
-            "ffill", axis=axis, inplace=inplace, limit=limit, downcast=downcast
+            "ffill",
+            axis=axis,
+            inplace=inplace,
+            limit=limit,
+            # error: Argument "downcast" to "_fillna_with_method" of "NDFrame"
+            # has incompatible type "Union[Dict[Any, Any], None,
+            # Literal[_NoDefault.no_default]]"; expected "Optional[Dict[Any, Any]]"
+            downcast=downcast,  # type: ignore[arg-type]
         )
 
     @final
@@ -7323,7 +7338,7 @@ def pad(
         axis: None | Axis = None,
         inplace: bool_t = False,
         limit: None | int = None,
-        downcast: dict | None = None,
+        downcast: dict | None | lib.NoDefault = lib.no_default,
     ) -> Self | None:
         """
         Synonym for :meth:`DataFrame.fillna` with ``method='ffill'``.
@@ -7352,7 +7367,7 @@ def bfill(
         axis: None | Axis = ...,
         inplace: Literal[False] = ...,
         limit: None | int = ...,
-        downcast: dict | None = ...,
+        downcast: dict | None | lib.NoDefault = ...,
     ) -> Self:
         ...
 
@@ -7363,7 +7378,7 @@ def bfill(
         axis: None | Axis = ...,
         inplace: Literal[True],
         limit: None | int = ...,
-        downcast: dict | None = ...,
+        downcast: dict | None | lib.NoDefault = ...,
     ) -> None:
         ...
 
@@ -7374,7 +7389,7 @@ def bfill(
         axis: None | Axis = ...,
         inplace: bool_t = ...,
         limit: None | int = ...,
-        downcast: dict | None = ...,
+        downcast: dict | None | lib.NoDefault = ...,
     ) -> Self | None:
         ...
 
@@ -7386,7 +7401,7 @@ def bfill(
         axis: None | Axis = None,
         inplace: bool_t = False,
         limit: None | int = None,
-        downcast: dict | None = None,
+        downcast: dict | None | lib.NoDefault = lib.no_default,
     ) -> Self | None:
         """
         Synonym for :meth:`DataFrame.fillna` with ``method='bfill'``.
@@ -7407,12 +7422,6 @@ def bfill(
         2    2.0
         3    2.0
         dtype: float64
-        >>> s.bfill(downcast='infer')
-        0    1
-        1    2
-        2    2
-        3    2
-        dtype: int64
         >>> s.bfill(limit=1)
         0    1.0
         1    NaN
@@ -7435,16 +7444,23 @@ def bfill(
         1   4.0   5.0
         2   4.0   7.0
         3   4.0   7.0
-        >>> df.bfill(downcast='infer', limit=1)
-              A	   B
-        0   1.0    5
-        1   NaN    5
-        2   4.0    7
-        3   4.0    7
-        """
-        self._deprecate_downcast(downcast)
+        >>> df.bfill(limit=1)
+              A     B
+        0   1.0   5.0
+        1   NaN   5.0
+        2   4.0   7.0
+        3   4.0   7.0
+        """
+        downcast = self._deprecate_downcast(downcast, "bfill")
         return self._pad_or_backfill(
-            "bfill", axis=axis, inplace=inplace, limit=limit, downcast=downcast
+            "bfill",
+            axis=axis,
+            inplace=inplace,
+            limit=limit,
+            # error: Argument "downcast" to "_fillna_with_method" of "NDFrame"
+            # has incompatible type "Union[Dict[Any, Any], None,
+            # Literal[_NoDefault.no_default]]"; expected "Optional[Dict[Any, Any]]"
+            downcast=downcast,  # type: ignore[arg-type]
         )
 
     @final
@@ -7455,7 +7471,7 @@ def backfill(
         axis: None | Axis = None,
         inplace: bool_t = False,
         limit: None | int = None,
-        downcast: dict | None = None,
+        downcast: dict | None | lib.NoDefault = lib.no_default,
     ) -> Self | None:
         """
         Synonym for :meth:`DataFrame.fillna` with ``method='bfill'``.
@@ -7780,7 +7796,7 @@ def interpolate(
         inplace: bool_t = False,
         limit_direction: Literal["forward", "backward", "both"] | None = None,
         limit_area: Literal["inside", "outside"] | None = None,
-        downcast: Literal["infer"] | None = None,
+        downcast: Literal["infer"] | None | lib.NoDefault = lib.no_default,
         **kwargs,
     ) -> Self | None:
         """
@@ -7851,6 +7867,9 @@ def interpolate(
 
         downcast : optional, 'infer' or None, defaults to None
             Downcast dtypes if possible.
+
+            .. deprecated:: 2.1.0
+
         ``**kwargs`` : optional
             Keyword arguments to pass on to the interpolating function.
 
@@ -7949,6 +7968,17 @@ def interpolate(
         3    16.0
         Name: d, dtype: float64
         """
+        if downcast is not lib.no_default:
+            # GH#40988
+            warnings.warn(
+                f"The 'downcast' keyword in {type(self).__name__}.interpolate "
+                "is deprecated and will be removed in a future version. "
+                "Call result.infer_objects(copy=False) on the result instead.",
+                FutureWarning,
+                stacklevel=find_stack_level(),
+            )
+        else:
+            downcast = None
         if downcast is not None and downcast != "infer":
             raise ValueError("downcast must be either None or 'infer'")
 
diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py
index 6c8e9c3fe9a75..78f8448cc7cb3 100644
--- a/pandas/core/groupby/generic.py
+++ b/pandas/core/groupby/generic.py
@@ -868,7 +868,7 @@ def fillna(
         axis: Axis | None | lib.NoDefault = lib.no_default,
         inplace: bool = False,
         limit: int | None = None,
-        downcast: dict | None = None,
+        downcast: dict | None | lib.NoDefault = lib.no_default,
     ) -> Series | None:
         """
         Fill NA/NaN values using the specified method within groups.
@@ -912,6 +912,8 @@ def fillna(
             or the string 'infer' which will try to downcast to an appropriate
             equal type (e.g. float64 to int64 if possible).
 
+            .. deprecated:: 2.1.0
+
         Returns
         -------
         Series
@@ -2390,7 +2392,7 @@ def fillna(
         axis: Axis | None | lib.NoDefault = lib.no_default,
         inplace: bool = False,
         limit: int | None = None,
-        downcast=None,
+        downcast=lib.no_default,
     ) -> DataFrame | None:
         """
         Fill NA/NaN values using the specified method within groups.
@@ -2434,6 +2436,8 @@ def fillna(
             or the string 'infer' which will try to downcast to an appropriate
             equal type (e.g. float64 to int64 if possible).
 
+            .. deprecated:: 2.1.0
+
         Returns
         -------
         DataFrame
diff --git a/pandas/core/resample.py b/pandas/core/resample.py
index 9566a2f113b36..382929958fb93 100644
--- a/pandas/core/resample.py
+++ b/pandas/core/resample.py
@@ -844,7 +844,7 @@ def interpolate(
         inplace: bool = False,
         limit_direction: Literal["forward", "backward", "both"] = "forward",
         limit_area=None,
-        downcast=None,
+        downcast=lib.no_default,
         **kwargs,
     ):
         """
@@ -917,6 +917,9 @@ def interpolate(
 
         downcast : optional, 'infer' or None, defaults to None
             Downcast dtypes if possible.
+
+            .. deprecated::2.1.0
+
         ``**kwargs`` : optional
             Keyword arguments to pass on to the interpolating function.
 
@@ -1000,6 +1003,7 @@ def interpolate(
         Note that the series erroneously increases between two anchors
         ``07:00:00`` and ``07:00:02``.
         """
+        assert downcast is lib.no_default  # just checking coverage
         result = self._upsample("asfreq")
         return result.interpolate(
             method=method,
diff --git a/pandas/core/reshape/pivot.py b/pandas/core/reshape/pivot.py
index 07985a2744069..2852ca8cf576a 100644
--- a/pandas/core/reshape/pivot.py
+++ b/pandas/core/reshape/pivot.py
@@ -207,26 +207,30 @@ def __internal_pivot_table(
                 to_unstack.append(i)
             else:
                 to_unstack.append(name)
-        table = agged.unstack(to_unstack)
+        table = agged.unstack(to_unstack, fill_value=fill_value)
 
     if not dropna:
         if isinstance(table.index, MultiIndex):
             m = MultiIndex.from_arrays(
                 cartesian_product(table.index.levels), names=table.index.names
             )
-            table = table.reindex(m, axis=0)
+            table = table.reindex(m, axis=0, fill_value=fill_value)
 
         if isinstance(table.columns, MultiIndex):
             m = MultiIndex.from_arrays(
                 cartesian_product(table.columns.levels), names=table.columns.names
             )
-            table = table.reindex(m, axis=1)
+            table = table.reindex(m, axis=1, fill_value=fill_value)
 
     if sort is True and isinstance(table, ABCDataFrame):
         table = table.sort_index(axis=1)
 
     if fill_value is not None:
-        table = table.fillna(fill_value, downcast="infer")
+        table = table.fillna(fill_value)
+        if aggfunc is len and not observed and lib.is_integer(fill_value):
+            # TODO: can we avoid this?  this used to be handled by
+            #  downcast="infer" in fillna
+            table = table.astype(np.int64)
 
     if margins:
         if dropna:


Test patch:
diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py
index 7f44e329e6778..c2f835212529f 100644
--- a/pandas/tests/arithmetic/test_timedelta64.py
+++ b/pandas/tests/arithmetic/test_timedelta64.py
@@ -2038,9 +2038,11 @@ def test_td64arr_div_numeric_array(
             expected = [tdser.iloc[0, n] / vector[n] for n in range(len(vector))]
             expected = tm.box_expected(expected, xbox).astype(object)
             # We specifically expect timedelta64("NaT") here, not pd.NA
-            expected[2] = expected[2].fillna(
-                np.timedelta64("NaT", "ns"), downcast=False
-            )
+            msg = "The 'downcast' keyword in fillna"
+            with tm.assert_produces_warning(FutureWarning, match=msg):
+                expected[2] = expected[2].fillna(
+                    np.timedelta64("NaT", "ns"), downcast=False
+                )
         else:
             expected = [tdser[n] / vector[n] for n in range(len(tdser))]
             expected = [
@@ -2122,7 +2124,9 @@ def test_td64arr_all_nat_div_object_dtype_numeric(self, box_with_array):
         if box_with_array is not Index:
             expected = tm.box_expected(expected, box_with_array).astype(object)
             if box_with_array in [Series, DataFrame]:
-                expected = expected.fillna(tdnat, downcast=False)  # GH#18463
+                msg = "The 'downcast' keyword in fillna is deprecated"
+                with tm.assert_produces_warning(FutureWarning, match=msg):
+                    expected = expected.fillna(tdnat, downcast=False)  # GH#18463
 
         result = left / right
         tm.assert_equal(result, expected)
diff --git a/pandas/tests/copy_view/test_interp_fillna.py b/pandas/tests/copy_view/test_interp_fillna.py
index 5d4fd75df830c..5a4b958b2148d 100644
--- a/pandas/tests/copy_view/test_interp_fillna.py
+++ b/pandas/tests/copy_view/test_interp_fillna.py
@@ -218,7 +218,9 @@ def test_fillna_inplace(using_copy_on_write, downcast):
     arr_a = get_array(df, "a")
     arr_b = get_array(df, "b")
 
-    df.fillna(5.5, inplace=True, downcast=downcast)
+    msg = "The 'downcast' keyword in fillna is deprecated"
+    with tm.assert_produces_warning(FutureWarning, match=msg):
+        df.fillna(5.5, inplace=True, downcast=downcast)
     assert np.shares_memory(get_array(df, "a"), arr_a)
     assert np.shares_memory(get_array(df, "b"), arr_b)
     if using_copy_on_write:
diff --git a/pandas/tests/frame/methods/test_fillna.py b/pandas/tests/frame/methods/test_fillna.py
index d340e048994a9..109520859af4d 100644
--- a/pandas/tests/frame/methods/test_fillna.py
+++ b/pandas/tests/frame/methods/test_fillna.py
@@ -292,20 +292,25 @@ def test_fillna_downcast(self):
         # GH#15277
         # infer int64 from float64
         df = DataFrame({"a": [1.0, np.nan]})
-        result = df.fillna(0, downcast="infer")
+        msg = "The 'downcast' keyword in fillna is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = df.fillna(0, downcast="infer")
         expected = DataFrame({"a": [1, 0]})
         tm.assert_frame_equal(result, expected)
 
         # infer int64 from float64 when fillna value is a dict
         df = DataFrame({"a": [1.0, np.nan]})
-        result = df.fillna({"a": 0}, downcast="infer")
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = df.fillna({"a": 0}, downcast="infer")
         expected = DataFrame({"a": [1, 0]})
         tm.assert_frame_equal(result, expected)
 
     def test_fillna_downcast_false(self, frame_or_series):
         # GH#45603 preserve object dtype with downcast=False
         obj = frame_or_series([1, 2, 3], dtype="object")
-        result = obj.fillna("", downcast=False)
+        msg = "The 'downcast' keyword in fillna"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = obj.fillna("", downcast=False)
         tm.assert_equal(result, obj)
 
     def test_fillna_downcast_noop(self, frame_or_series):
@@ -316,7 +321,7 @@ def test_fillna_downcast_noop(self, frame_or_series):
 
         obj = frame_or_series([1, 2, 3], dtype=np.int64)
 
-        msg = "downcast other than None, False, and 'infer' are deprecated"
+        msg = "The 'downcast' keyword in fillna"
         with tm.assert_produces_warning(FutureWarning, match=msg):
             # GH#40988
             res = obj.fillna("foo", downcast=np.dtype(np.int32))
@@ -324,7 +329,8 @@ def test_fillna_downcast_noop(self, frame_or_series):
         tm.assert_equal(res, expected)
 
         obj2 = obj.astype(np.float64)
-        res2 = obj2.fillna("foo", downcast="infer")
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            res2 = obj2.fillna("foo", downcast="infer")
         expected2 = obj  # get back int64
         tm.assert_equal(res2, expected2)
 
@@ -648,7 +654,7 @@ def test_fillna_downcast_dict(self):
         # GH#40809
         df = DataFrame({"col1": [1, np.nan]})
 
-        msg = "downcast entries other than None, False, and 'infer' are deprecated"
+        msg = "The 'downcast' keyword in fillna"
         with tm.assert_produces_warning(FutureWarning, match=msg):
             result = df.fillna({"col1": 2}, downcast={"col1": "int64"})
         expected = DataFrame({"col1": [1, 2]})
diff --git a/pandas/tests/frame/methods/test_interpolate.py b/pandas/tests/frame/methods/test_interpolate.py
index 429f3678c34f9..ac862e5673411 100644
--- a/pandas/tests/frame/methods/test_interpolate.py
+++ b/pandas/tests/frame/methods/test_interpolate.py
@@ -165,7 +165,9 @@ def test_interp_combo(self):
         expected = Series([1.0, 2.0, 3.0, 4.0], name="A")
         tm.assert_series_equal(result, expected)
 
-        result = df["A"].interpolate(downcast="infer")
+        msg = "The 'downcast' keyword in Series.interpolate is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = df["A"].interpolate(downcast="infer")
         expected = Series([1, 2, 3, 4], name="A")
         tm.assert_series_equal(result, expected)
 
@@ -181,10 +183,14 @@ def test_inerpolate_invalid_downcast(self):
         )
 
         msg = "downcast must be either None or 'infer'"
+        msg2 = "The 'downcast' keyword in DataFrame.interpolate is deprecated"
+        msg3 = "The 'downcast' keyword in Series.interpolate is deprecated"
         with pytest.raises(ValueError, match=msg):
-            df.interpolate(downcast="int64")
+            with tm.assert_produces_warning(FutureWarning, match=msg2):
+                df.interpolate(downcast="int64")
         with pytest.raises(ValueError, match=msg):
-            df["A"].interpolate(downcast="int64")
+            with tm.assert_produces_warning(FutureWarning, match=msg3):
+                df["A"].interpolate(downcast="int64")
 
     def test_interp_nan_idx(self):
         df = DataFrame({"A": [1, 2, np.nan, 4], "B": [np.nan, 2, 3, 4]})
@@ -246,7 +252,9 @@ def test_interp_alt_scipy(self):
         expected.loc[5, "A"] = 6
         tm.assert_frame_equal(result, expected)
 
-        result = df.interpolate(method="barycentric", downcast="infer")
+        msg = "The 'downcast' keyword in DataFrame.interpolate is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = df.interpolate(method="barycentric", downcast="infer")
         tm.assert_frame_equal(result, expected.astype(np.int64))
 
         result = df.interpolate(method="krogh")
@@ -370,7 +378,9 @@ def test_interp_inplace(self, using_copy_on_write):
             tm.assert_frame_equal(result, expected)
 
         result = df.copy()
-        return_value = result["a"].interpolate(inplace=True, downcast="infer")
+        msg = "The 'downcast' keyword in Series.interpolate is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            return_value = result["a"].interpolate(inplace=True, downcast="infer")
         assert return_value is None
         if using_copy_on_write:
             tm.assert_frame_equal(result, expected_cow)
@@ -406,11 +416,14 @@ def test_interp_ignore_all_good(self):
             }
         )
 
-        result = df.interpolate(downcast=None)
+        msg = "The 'downcast' keyword in DataFrame.interpolate is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = df.interpolate(downcast=None)
         tm.assert_frame_equal(result, expected)
 
         # all good
-        result = df[["B", "D"]].interpolate(downcast=None)
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = df[["B", "D"]].interpolate(downcast=None)
         tm.assert_frame_equal(result, df[["B", "D"]])
 
     def test_interp_time_inplace_axis(self):
diff --git a/pandas/tests/frame/methods/test_quantile.py b/pandas/tests/frame/methods/test_quantile.py
index d50b3e45969f0..0d9f4bd77d137 100644
--- a/pandas/tests/frame/methods/test_quantile.py
+++ b/pandas/tests/frame/methods/test_quantile.py
@@ -736,7 +736,9 @@ def test_quantile_empty_no_rows_dt64(self, interp_method):
         exp = exp.astype(object)
         if interpolation == "nearest":
             # GH#18463 TODO: would we prefer NaTs here?
-            exp = exp.fillna(np.nan, downcast=False)
+            msg = "The 'downcast' keyword in fillna is deprecated"
+            with tm.assert_produces_warning(FutureWarning, match=msg):
+                exp = exp.fillna(np.nan, downcast=False)
         tm.assert_series_equal(res, exp)
 
         # both dt64tz
diff --git a/pandas/tests/frame/test_logical_ops.py b/pandas/tests/frame/test_logical_ops.py
index a58b79b5db111..2cc3b67e7ac02 100644
--- a/pandas/tests/frame/test_logical_ops.py
+++ b/pandas/tests/frame/test_logical_ops.py
@@ -165,7 +165,9 @@ def test_logical_with_nas(self):
         expected = Series([True, True])
         tm.assert_series_equal(result, expected)
 
-        result = d["a"].fillna(False, downcast=False) | d["b"]
+        msg = "The 'downcast' keyword in fillna is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = d["a"].fillna(False, downcast=False) | d["b"]
         expected = Series([True, True])
         tm.assert_series_equal(result, expected)
 
diff --git a/pandas/tests/groupby/test_categorical.py b/pandas/tests/groupby/test_categorical.py
index c0704d9684574..b25950192018d 100644
--- a/pandas/tests/groupby/test_categorical.py
+++ b/pandas/tests/groupby/test_categorical.py
@@ -1235,7 +1235,9 @@ def test_seriesgroupby_observed_false_or_none(df_cat, observed, operation):
 
     expected = Series(data=[2, 4, np.nan, 1, np.nan, 3], index=index, name="C")
     if operation == "agg":
-        expected = expected.fillna(0, downcast="infer")
+        msg = "The 'downcast' keyword in fillna is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            expected = expected.fillna(0, downcast="infer")
     grouped = df_cat.groupby(["A", "B"], observed=observed)["C"]
     result = getattr(grouped, operation)(sum)
     tm.assert_series_equal(result, expected)
diff --git a/pandas/tests/groupby/transform/test_transform.py b/pandas/tests/groupby/transform/test_transform.py
index 5caf3d2ac1d8b..cf41b4ff57331 100644
--- a/pandas/tests/groupby/transform/test_transform.py
+++ b/pandas/tests/groupby/transform/test_transform.py
@@ -764,10 +764,12 @@ def test_cython_transform_frame(request, op, args, targop, df_fix, gb_target):
 
     expected = expected.sort_index(axis=1)
     if op == "shift":
-        expected["string_missing"] = expected["string_missing"].fillna(
-            np.nan, downcast=False
-        )
-        expected["string"] = expected["string"].fillna(np.nan, downcast=False)
+        depr_msg = "The 'downcast' keyword in fillna is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=depr_msg):
+            expected["string_missing"] = expected["string_missing"].fillna(
+                np.nan, downcast=False
+            )
+            expected["string"] = expected["string"].fillna(np.nan, downcast=False)
 
     result = gb[expected.columns].transform(op, *args).sort_index(axis=1)
     tm.assert_frame_equal(result, expected)
@@ -835,7 +837,9 @@ def test_cython_transform_frame_column(
         expected = gb[c].apply(targop)
         expected.name = c
         if c in ["string_missing", "string"]:
-            expected = expected.fillna(np.nan, downcast=False)
+            depr_msg = "The 'downcast' keyword in fillna is deprecated"
+            with tm.assert_produces_warning(FutureWarning, match=depr_msg):
+                expected = expected.fillna(np.nan, downcast=False)
 
         res = gb[c].transform(op, *args)
         tm.assert_series_equal(expected, res)
diff --git a/pandas/tests/io/json/test_pandas.py b/pandas/tests/io/json/test_pandas.py
index b6b21f9962876..11909bf56f05c 100644
--- a/pandas/tests/io/json/test_pandas.py
+++ b/pandas/tests/io/json/test_pandas.py
@@ -994,7 +994,9 @@ def test_round_trip_exception(self, datapath):
 
         result = read_json(StringIO(s))
         res = result.reindex(index=df.index, columns=df.columns)
-        res = res.fillna(np.nan, downcast=False)
+        msg = "The 'downcast' keyword in fillna is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            res = res.fillna(np.nan, downcast=False)
         tm.assert_frame_equal(res, df)
 
     @pytest.mark.network
diff --git a/pandas/tests/io/test_html.py b/pandas/tests/io/test_html.py
index d17e4b08b5a4d..0c1f3b58ad9df 100644
--- a/pandas/tests/io/test_html.py
+++ b/pandas/tests/io/test_html.py
@@ -1497,7 +1497,7 @@ def test_extract_links(self, arg):
 
         result = self.read_html(gh_13141_data, extract_links=arg)[0]
         expected = DataFrame([data_exp, foot_exp], columns=head_exp)
-        expected = expected.fillna(np.nan, downcast=False)
+        expected = expected.fillna(np.nan)
         tm.assert_frame_equal(result, expected)
 
     def test_extract_links_bad(self, spam_data):
diff --git a/pandas/tests/series/methods/test_fillna.py b/pandas/tests/series/methods/test_fillna.py
index 7665e4d015a45..aaffd52b78f95 100644
--- a/pandas/tests/series/methods/test_fillna.py
+++ b/pandas/tests/series/methods/test_fillna.py
@@ -175,13 +175,16 @@ def test_fillna_downcast(self):
         # GH#15277
         # infer int64 from float64
         ser = Series([1.0, np.nan])
-        result = ser.fillna(0, downcast="infer")
+        msg = "The 'downcast' keyword in fillna is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = ser.fillna(0, downcast="infer")
         expected = Series([1, 0])
         tm.assert_series_equal(result, expected)
 
         # infer int64 from float64 when fillna value is a dict
         ser = Series([1.0, np.nan])
-        result = ser.fillna({1: 0}, downcast="infer")
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = ser.fillna({1: 0}, downcast="infer")
         expected = Series([1, 0])
         tm.assert_series_equal(result, expected)
 
@@ -194,15 +197,21 @@ def test_fillna_downcast_infer_objects_to_numeric(self):
 
         ser = Series(arr)
 
-        res = ser.fillna(3, downcast="infer")
+        msg = "The 'downcast' keyword in fillna is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            res = ser.fillna(3, downcast="infer")
         expected = Series(np.arange(5), dtype=np.int64)
         tm.assert_series_equal(res, expected)
 
-        res = ser.ffill(downcast="infer")
+        msg = "The 'downcast' keyword in ffill is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            res = ser.ffill(downcast="infer")
         expected = Series([0, 1, 2, 2, 4], dtype=np.int64)
         tm.assert_series_equal(res, expected)
 
-        res = ser.bfill(downcast="infer")
+        msg = "The 'downcast' keyword in bfill is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            res = ser.bfill(downcast="infer")
         expected = Series([0, 1, 2, 4, 4], dtype=np.int64)
         tm.assert_series_equal(res, expected)
 
@@ -210,14 +219,20 @@ def test_fillna_downcast_infer_objects_to_numeric(self):
         ser[2] = 2.5
 
         expected = Series([0, 1, 2.5, 3, 4], dtype=np.float64)
-        res = ser.fillna(3, downcast="infer")
+        msg = "The 'downcast' keyword in fillna is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            res = ser.fillna(3, downcast="infer")
         tm.assert_series_equal(res, expected)
 
-        res = ser.ffill(downcast="infer")
+        msg = "The 'downcast' keyword in ffill is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            res = ser.ffill(downcast="infer")
         expected = Series([0, 1, 2.5, 2.5, 4], dtype=np.float64)
         tm.assert_series_equal(res, expected)
 
-        res = ser.bfill(downcast="infer")
+        msg = "The 'downcast' keyword in bfill is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            res = ser.bfill(downcast="infer")
         expected = Series([0, 1, 2.5, 4, 4], dtype=np.float64)
         tm.assert_series_equal(res, expected)
 
diff --git a/pandas/tests/series/methods/test_interpolate.py b/pandas/tests/series/methods/test_interpolate.py
index abd4cec5ad45c..4a8bf554742dd 100644
--- a/pandas/tests/series/methods/test_interpolate.py
+++ b/pandas/tests/series/methods/test_interpolate.py
@@ -290,21 +290,25 @@ def test_interp_scipy_basic(self):
         result = s.interpolate(method="slinear")
         tm.assert_series_equal(result, expected)
 
-        result = s.interpolate(method="slinear", downcast="infer")
+        msg = "The 'downcast' keyword in Series.interpolate is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = s.interpolate(method="slinear", downcast="infer")
         tm.assert_series_equal(result, expected)
         # nearest
         expected = Series([1, 3, 3, 12, 12, 25])
         result = s.interpolate(method="nearest")
         tm.assert_series_equal(result, expected.astype("float"))
 
-        result = s.interpolate(method="nearest", downcast="infer")
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = s.interpolate(method="nearest", downcast="infer")
         tm.assert_series_equal(result, expected)
         # zero
         expected = Series([1, 3, 3, 12, 12, 25])
         result = s.interpolate(method="zero")
         tm.assert_series_equal(result, expected.astype("float"))
 
-        result = s.interpolate(method="zero", downcast="infer")
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = s.interpolate(method="zero", downcast="infer")
         tm.assert_series_equal(result, expected)
         # quadratic
         # GH #15662.
@@ -312,7 +316,8 @@ def test_interp_scipy_basic(self):
         result = s.interpolate(method="quadratic")
         tm.assert_series_equal(result, expected)
 
-        result = s.interpolate(method="quadratic", downcast="infer")
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = s.interpolate(method="quadratic", downcast="infer")
         tm.assert_series_equal(result, expected)
         # cubic
         expected = Series([1.0, 3.0, 6.8, 12.0, 18.2, 25.0])
diff --git a/pandas/tests/series/methods/test_reindex.py b/pandas/tests/series/methods/test_reindex.py
index c801528e6ff97..cf042b310ff63 100644
--- a/pandas/tests/series/methods/test_reindex.py
+++ b/pandas/tests/series/methods/test_reindex.py
@@ -143,7 +143,9 @@ def test_reindex_pad2():
     result = s.reindex(new_index).ffill()
     tm.assert_series_equal(result, expected.astype("float64"))
 
-    result = s.reindex(new_index).ffill(downcast="infer")
+    msg = "The 'downcast' keyword in ffill is deprecated"
+    with tm.assert_produces_warning(FutureWarning, match=msg):
+        result = s.reindex(new_index).ffill(downcast="infer")
     tm.assert_series_equal(result, expected)
 
     expected = Series([1, 5, 3, 5], index=new_index)