Repository: pandas-dev/pandas (python)

Issue:
DEPR: observed=False default for DataFrame.pivot_table
This will keep the default in line with groupby from #43999

Pull request:
DEPR: Default of observed=False in DataFrame.pivot_table
- [x] closes #56236 (Replace xxxx with the GitHub issue number)
- [x] [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
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [x] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


Patch:
diff --git a/doc/source/user_guide/categorical.rst b/doc/source/user_guide/categorical.rst
index 34d04745cc..8fb991dca0 100644
--- a/doc/source/user_guide/categorical.rst
+++ b/doc/source/user_guide/categorical.rst
@@ -647,7 +647,7 @@ Pivot tables:
 
     raw_cat = pd.Categorical(["a", "a", "b", "b"], categories=["a", "b", "c"])
     df = pd.DataFrame({"A": raw_cat, "B": ["c", "d", "c", "d"], "values": [1, 2, 3, 4]})
-    pd.pivot_table(df, values="values", index=["A", "B"])
+    pd.pivot_table(df, values="values", index=["A", "B"], observed=False)
 
 Data munging
 ------------
diff --git a/doc/source/whatsnew/v0.23.0.rst b/doc/source/whatsnew/v0.23.0.rst
index cdffc6968a..808741ccf4 100644
--- a/doc/source/whatsnew/v0.23.0.rst
+++ b/doc/source/whatsnew/v0.23.0.rst
@@ -286,12 +286,33 @@ For pivoting operations, this behavior is *already* controlled by the ``dropna``
    df = pd.DataFrame({"A": cat1, "B": cat2, "values": [1, 2, 3, 4]})
    df
 
-.. ipython:: python
 
-   pd.pivot_table(df, values='values', index=['A', 'B'],
-                  dropna=True)
-   pd.pivot_table(df, values='values', index=['A', 'B'],
-                  dropna=False)
+.. code-block:: ipython
+
+    In [1]: pd.pivot_table(df, values='values', index=['A', 'B'], dropna=True)
+
+    Out[1]:
+         values
+    A B
+    a c     1.0
+      d     2.0
+    b c     3.0
+      d     4.0
+
+    In [2]: pd.pivot_table(df, values='values', index=['A', 'B'], dropna=False)
+
+    Out[2]:
+         values
+    A B
+    a c     1.0
+      d     2.0
+      y     NaN
+    b c     3.0
+      d     4.0
+      y     NaN
+    z c     NaN
+      d     NaN
+      y     NaN
 
 
 .. _whatsnew_0230.enhancements.window_raw:
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index ade87c4215..5ee2bb1778 100644
--- a/doc/source/whatsnew/v2.2.0.rst
+++ b/doc/source/whatsnew/v2.2.0.rst
@@ -226,6 +226,7 @@ Other enhancements
 - Allow passing ``read_only``, ``data_only`` and ``keep_links`` arguments to openpyxl using ``engine_kwargs`` of :func:`read_excel` (:issue:`55027`)
 - DataFrame.apply now allows the usage of numba (via ``engine="numba"``) to JIT compile the passed function, allowing for potential speedups (:issue:`54666`)
 - Implement masked algorithms for :meth:`Series.value_counts` (:issue:`54984`)
+- Improved error message that appears in :meth:`DatetimeIndex.to_period` with frequencies which are not supported as period frequencies, such as "BMS" (:issue:`56243`)
 - Improved error message when constructing :class:`Period` with invalid offsets such as "QS" (:issue:`55785`)
 
 .. ---------------------------------------------------------------------------
@@ -434,6 +435,7 @@ Other Deprecations
 - Deprecated the ``ordinal`` keyword in :class:`PeriodIndex`, use :meth:`PeriodIndex.from_ordinals` instead (:issue:`55960`)
 - Deprecated the ``unit`` keyword in :class:`TimedeltaIndex` construction, use :func:`to_timedelta` instead (:issue:`55499`)
 - Deprecated the behavior of :meth:`Series.value_counts` and :meth:`Index.value_counts` with object dtype; in a future version these will not perform dtype inference on the resulting :class:`Index`, do ``result.index = result.index.infer_objects()`` to retain the old behavior (:issue:`56161`)
+- Deprecated the default of ``observed=False`` in :meth:`DataFrame.pivot_table`; will be ``True`` in a future version (:issue:`56236`)
 - Deprecated the extension test classes ``BaseNoReduceTests``, ``BaseBooleanReduceTests``, and ``BaseNumericReduceTests``, use ``BaseReduceTests`` instead (:issue:`54663`)
 - Deprecated the option ``mode.data_manager`` and the ``ArrayManager``; only the ``BlockManager`` will be available in future versions (:issue:`55043`)
 - Deprecated the previous implementation of :class:`DataFrame.stack`; specify ``future_stack=True`` to adopt the future version (:issue:`53515`)
diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py
index 57b244e8d0..a8c21cfbb6 100644
--- a/pandas/core/arrays/period.py
+++ b/pandas/core/arrays/period.py
@@ -1174,7 +1174,12 @@ def dt64arr_to_periodarr(
 
     reso = get_unit_from_dtype(data.dtype)
     freq = Period._maybe_convert_freq(freq)
-    base = freq._period_dtype_code
+    try:
+        base = freq._period_dtype_code
+    except (AttributeError, TypeError):
+        # AttributeError: _period_dtype_code might not exist
+        # TypeError: _period_dtype_code might intentionally raise
+        raise TypeError(f"{freq.name} is not supported as period frequency")
     return c_dt64arr_to_periodarr(data.view("i8"), base, tz, reso=reso), freq
 
 
diff --git a/pandas/core/computation/eval.py b/pandas/core/computation/eval.py
index 0c99e5e7bd..f1fe528de0 100644
--- a/pandas/core/computation/eval.py
+++ b/pandas/core/computation/eval.py
@@ -388,17 +388,10 @@ def eval(
             # we will ignore numpy warnings here; e.g. if trying
             # to use a non-numeric indexer
             try:
-                with warnings.catch_warnings(record=True):
-                    warnings.filterwarnings(
-                        "always", "Setting a value on a view", FutureWarning
-                    )
-                    # TODO: Filter the warnings we actually care about here.
-                    if inplace and isinstance(target, NDFrame):
-                        target.loc[:, assigner] = ret
-                    else:
-                        target[  # pyright: ignore[reportGeneralTypeIssues]
-                            assigner
-                        ] = ret
+                if inplace and isinstance(target, NDFrame):
+                    target.loc[:, assigner] = ret
+                else:
+                    target[assigner] = ret  # pyright: ignore[reportGeneralTypeIssues]
             except (TypeError, IndexError) as err:
                 raise ValueError("Cannot assign expression output to target") from err
 
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index 008c1e0d10..3edfea4480 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -4230,15 +4230,14 @@ class DataFrame(NDFrame, OpsMixin):
                 warnings.warn(
                     _chained_assignment_msg, ChainedAssignmentError, stacklevel=2
                 )
-        # elif not PYPY and not using_copy_on_write():
-        elif not PYPY and warn_copy_on_write():
-            if sys.getrefcount(self) <= 3:  # and (
-                #     warn_copy_on_write()
-                #     or (
-                #         not warn_copy_on_write()
-                #         and self._mgr.blocks[0].refs.has_reference()
-                #     )
-                # ):
+        elif not PYPY and not using_copy_on_write():
+            if sys.getrefcount(self) <= 3 and (
+                warn_copy_on_write()
+                or (
+                    not warn_copy_on_write()
+                    and any(b.refs.has_reference() for b in self._mgr.blocks)  # type: ignore[union-attr]
+                )
+            ):
                 warnings.warn(
                     _chained_assignment_warning_msg, FutureWarning, stacklevel=2
                 )
@@ -8904,7 +8903,7 @@ class DataFrame(NDFrame, OpsMixin):
                     ChainedAssignmentError,
                     stacklevel=2,
                 )
-        elif not PYPY and not using_copy_on_write():
+        elif not PYPY and not using_copy_on_write() and self._is_view_after_cow_rules():
             if sys.getrefcount(self) <= REF_COUNT:
                 warnings.warn(
                     _chained_assignment_warning_method_msg,
@@ -9297,6 +9296,11 @@ class DataFrame(NDFrame, OpsMixin):
             If True: only show observed values for categorical groupers.
             If False: show all values for categorical groupers.
 
+            .. deprecated:: 2.2.0
+
+                The default value of ``False`` is deprecated and will change to
+                ``True`` in a future version of pandas.
+
         sort : bool, default True
             Specifies if the result should be sorted.
 
@@ -9407,7 +9411,7 @@ class DataFrame(NDFrame, OpsMixin):
         margins: bool = False,
         dropna: bool = True,
         margins_name: Level = "All",
-        observed: bool = False,
+        observed: bool | lib.NoDefault = lib.no_default,
         sort: bool = True,
     ) -> DataFrame:
         from pandas.core.reshape.pivot import pivot_table
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
index 376354aede..e6dcb634f7 100644
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -650,12 +650,17 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         Used in :meth:`DataFrame.eval`.
         """
         from pandas.core.computation.parsing import clean_column_name
+        from pandas.core.series import Series
 
         if isinstance(self, ABCSeries):
             return {clean_column_name(self.name): self}
 
         return {
-            clean_column_name(k): v for k, v in self.items() if not isinstance(k, int)
+            clean_column_name(k): Series(
+                v, copy=False, index=self.index, name=k
+            ).__finalize__(self)
+            for k, v in zip(self.columns, self._iter_column_arrays())
+            if not isinstance(k, int)
         }
 
     @final
@@ -663,6 +668,14 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
     def _info_axis(self) -> Index:
         return getattr(self, self._info_axis_name)
 
+    def _is_view_after_cow_rules(self):
+        # Only to be used in cases of chained assignment checks, this is a
+        # simplified check that assumes that either the whole object is a view
+        # or a copy
+        if len(self._mgr.blocks) == 0:  # type: ignore[union-attr]
+            return False
+        return self._mgr.blocks[0].refs.has_reference()  # type: ignore[union-attr]
+
     @property
     def shape(self) -> tuple[int, ...]:
         """
@@ -6681,6 +6694,21 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         and index are copied). Any changes to the data of the original
         will be reflected in the shallow copy (and vice versa).
 
+        .. note::
+            The ``deep=False`` behaviour as described above will change
+            in pandas 3.0. `Copy-on-Write
+            <https://pandas.pydata.org/docs/dev/user_guide/copy_on_write.html>`__
+            will be enabled by default, which means that the "shallow" copy
+            is that is returned with ``deep=False`` will still avoid making
+            an eager copy, but changes to the data of the original will *no*
+            longer be reflected in the shallow copy (or vice versa). Instead,
+            it makes use of a lazy (deferred) copy mechanism that will copy
+            the data only when any changes to the original or shallow copy is
+            made.
+
+            You can already get the future behavior and improvements through
+            enabling copy on write ``pd.options.mode.copy_on_write = True``
+
         Parameters
         ----------
         deep : bool, default True
@@ -6750,7 +6778,8 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         False
 
         Updates to the data shared by shallow copy and original is reflected
-        in both; deep copy remains unchanged.
+        in both (NOTE: this will no longer be true for pandas >= 3.0);
+        deep copy remains unchanged.
 
         >>> s.iloc[0] = 3
         >>> shallow.iloc[1] = 4
@@ -6783,7 +6812,8 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         1     [3, 4]
         dtype: object
 
-        ** Copy-on-Write is set to true: **
+        **Copy-on-Write is set to true**, the shallow copy is not modified
+        when the original data is changed:
 
         >>> with pd.option_context("mode.copy_on_write", True):
         ...     s = pd.Series([1, 2], index=["a", "b"])
@@ -7263,7 +7293,11 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
                         ChainedAssignmentError,
                         stacklevel=2,
                     )
-            elif not PYPY and not using_copy_on_write():
+            elif (
+                not PYPY
+                and not using_copy_on_write()
+                and self._is_view_after_cow_rules()
+            ):
                 ctr = sys.getrefcount(self)
                 ref_count = REF_COUNT
                 if isinstance(self, ABCSeries) and _check_cacher(self):
@@ -7545,7 +7579,11 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
                         ChainedAssignmentError,
                         stacklevel=2,
                     )
-            elif not PYPY and not using_copy_on_write():
+            elif (
+                not PYPY
+                and not using_copy_on_write()
+                and self._is_view_after_cow_rules()
+            ):
                 ctr = sys.getrefcount(self)
                 ref_count = REF_COUNT
                 if isinstance(self, ABCSeries) and _check_cacher(self):
@@ -7728,7 +7766,11 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
                         ChainedAssignmentError,
                         stacklevel=2,
                     )
-            elif not PYPY and not using_copy_on_write():
+            elif (
+                not PYPY
+                and not using_copy_on_write()
+                and self._is_view_after_cow_rules()
+            ):
                 ctr = sys.getrefcount(self)
                 ref_count = REF_COUNT
                 if isinstance(self, ABCSeries) and _check_cacher(self):
@@ -7894,7 +7936,11 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
                         ChainedAssignmentError,
                         stacklevel=2,
                     )
-            elif not PYPY and not using_copy_on_write():
+            elif (
+                not PYPY
+                and not using_copy_on_write()
+                and self._is_view_after_cow_rules()
+            ):
                 ctr = sys.getrefcount(self)
                 ref_count = REF_COUNT
                 if isinstance(self, ABCSeries) and _check_cacher(self):
@@ -8335,7 +8381,11 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
                         ChainedAssignmentError,
                         stacklevel=2,
                     )
-            elif not PYPY and not using_copy_on_write():
+            elif (
+                not PYPY
+                and not using_copy_on_write()
+                and self._is_view_after_cow_rules()
+            ):
                 ctr = sys.getrefcount(self)
                 ref_count = REF_COUNT
                 if isinstance(self, ABCSeries) and _check_cacher(self):
@@ -8973,7 +9023,11 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
                         ChainedAssignmentError,
                         stacklevel=2,
                     )
-            elif not PYPY and not using_copy_on_write():
+            elif (
+                not PYPY
+                and not using_copy_on_write()
+                and self._is_view_after_cow_rules()
+            ):
                 ctr = sys.getrefcount(self)
                 ref_count = REF_COUNT
                 if isinstance(self, ABCSeries) and hasattr(self, "_cacher"):
@@ -10578,6 +10632,7 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         inplace: bool_t = False,
         axis: Axis | None = None,
         level=None,
+        warn: bool_t = True,
     ):
         """
         Equivalent to public method `where`, except that `other` is not
@@ -10708,7 +10763,7 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
             # we may have different type blocks come out of putmask, so
             # reconstruct the block manager
 
-            new_data = self._mgr.putmask(mask=cond, new=other, align=align)
+            new_data = self._mgr.putmask(mask=cond, new=other, align=align, warn=warn)
             result = self._constructor_from_mgr(new_data, axes=new_data.axes)
             return self._update_inplace(result)
 
@@ -10920,7 +10975,11 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
                         ChainedAssignmentError,
                         stacklevel=2,
                     )
-            elif not PYPY and not using_copy_on_write():
+            elif (
+                not PYPY
+                and not using_copy_on_write()
+                and self._is_view_after_cow_rules()
+            ):
                 ctr = sys.getrefcount(self)
                 ref_count = REF_COUNT
                 if isinstance(self, ABCSeries) and hasattr(self, "_cacher"):
@@ -10999,7 +11058,11 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
                         ChainedAssignmentError,
                         stacklevel=2,
                     )
-            elif not PYPY and not using_copy_on_write():
+            elif (
+                not PYPY
+                and not using_copy_on_write()
+                and self._is_view_after_cow_rules()
+            ):
                 ctr = sys.getrefcount(self)
                 ref_count = REF_COUNT
                 if isinstance(self, ABCSeries) and hasattr(self, "_cacher"):
@@ -12641,6 +12704,7 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
             and result._indexed_same(self)
             and result.dtype == self.dtype
             and not using_copy_on_write()
+            and not (warn_copy_on_write() and not warn)
         ):
             # GH#36498 this inplace op can _actually_ be inplace.
             # Item "ArrayManager" of "Union[ArrayManager, SingleArrayManager,
diff --git a/pandas/core/internals/base.py b/pandas/core/internals/base.py
index fe366c7375..664856b828 100644
--- a/pandas/core/internals/base.py
+++ b/pandas/core/internals/base.py
@@ -14,7 +14,10 @@ from typing import (
 
 import numpy as np
 
-from pandas._config import using_copy_on_write
+from pandas._config import (
+    using_copy_on_write,
+    warn_copy_on_write,
+)
 
 from pandas._libs import (
     algos as libalgos,
@@ -49,6 +52,16 @@ if TYPE_CHECKING:
     )
 
 
+class _AlreadyWarned:
+    def __init__(self):
+        # This class is used on the manager level to the block level to
+        # ensure that we warn only once. The block method can update the
+        # warned_already option without returning a value to keep the
+        # interface consistent. This is only a temporary solution for
+        # CoW warnings.
+        self.warned_already = False
+
+
 class DataManager(PandasObject):
     # TODO share more methods/attributes
 
@@ -196,19 +209,26 @@ class DataManager(PandasObject):
         )
 
     @final
-    def putmask(self, mask, new, align: bool = True) -> Self:
+    def putmask(self, mask, new, align: bool = True, warn: bool = True) -> Self:
         if align:
             align_keys = ["new", "mask"]
         else:
             align_keys = ["mask"]
             new = extract_array(new, extract_numpy=True)
 
+        already_warned = None
+        if warn_copy_on_write():
+            already_warned = _AlreadyWarned()
+            if not warn:
+                already_warned.warned_already = True
+
         return self.apply_with_block(
             "putmask",
             align_keys=align_keys,
             mask=mask,
             new=new,
             using_cow=using_copy_on_write(),
+            already_warned=already_warned,
         )
 
     @final
diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py
index 535d18f99f..a06d266870 100644
--- a/pandas/core/internals/blocks.py
+++ b/pandas/core/internals/blocks.py
@@ -18,6 +18,7 @@ import numpy as np
 from pandas._config import (
     get_option,
     using_copy_on_write,
+    warn_copy_on_write,
 )
 
 from pandas._libs import (
@@ -136,6 +137,29 @@ if TYPE_CHECKING:
 _dtype_obj = np.dtype("object")
 
 
+COW_WARNING_GENERAL_MSG = """\
+Setting a value on a view: behaviour will change in pandas 3.0.
+You are mutating a Series or DataFrame object, and currently this mutation will
+also have effect on other Series or DataFrame objects that share data with this
+object. In pandas 3.0 (with Copy-on-Write), updating one Series or DataFrame object
+will never modify another.
+"""
+
+
+COW_WARNING_SETITEM_MSG = """\
+Setting a value on a view: behaviour will change in pandas 3.0.
+Currently, the mutation will also have effect on the object that shares data
+with this object. For example, when setting a value in a Series that was
+extracted from a column of a DataFrame, that DataFrame will also be updated:
+
+    ser = df["col"]
+    ser[0] = 0     <--- in pandas 2, this also updates `df`
+
+In pandas 3.0 (with Copy-on-Write), updating one Series/DataFrame will never
+modify another, and thus in the example above, `df` will not be changed.
+"""
+
+
 def maybe_split(meth: F) -> F:
     """
     If we have a multi-column block, split and operate block-wise.  Otherwise
@@ -1355,7 +1379,9 @@ class Block(PandasObject, libinternals.Block):
             values[indexer] = casted
         return self
 
-    def putmask(self, mask, new, using_cow: bool = False) -> list[Block]:
+    def putmask(
+        self, mask, new, using_cow: bool = False, already_warned=None
+    ) -> list[Block]:
         """
         putmask the data to the block; it is possible that we may create a
         new dtype of block
@@ -1388,6 +1414,19 @@ class Block(PandasObject, libinternals.Block):
                 return [self.copy(deep=False)]
             return [self]
 
+        if (
+            warn_copy_on_write()
+            and already_warned is not None
+            and not already_warned.warned_already
+        ):
+            if self.refs.has_reference():
+                warnings.warn(
+                    COW_WARNING_GENERAL_MSG,
+                    FutureWarning,
+                    stacklevel=find_stack_level(),
+                )
+                already_warned.warned_already = True
+
         try:
             casted = np_can_hold_element(values.dtype, new)
 
@@ -2020,7 +2059,9 @@ class EABackedBlock(Block):
         return [nb]
 
     @final
-    def putmask(self, mask, new, using_cow: bool = False) -> list[Block]:
+    def putmask(
+        self, mask, new, using_cow: bool = False, already_warned=None
+    ) -> list[Block]:
         """
         See Block.putmask.__doc__
         """
@@ -2038,6 +2079,19 @@ class EABackedBlock(Block):
                 return [self.copy(deep=False)]
             return [self]
 
+        if (
+            warn_copy_on_write()
+            and already_warned is not None
+            and not already_warned.warned_already
+        ):
+            if self.refs.has_reference():
+                warnings.warn(
+                    COW_WARNING_GENERAL_MSG,
+                    FutureWarning,
+                    stacklevel=find_stack_level(),
+                )
+                already_warned.warned_already = True
+
         self = self._maybe_copy(using_cow, inplace=True)
         values = self.values
         if values.ndim == 2:
diff --git a/pandas/core/internals/managers.py b/pandas/core/internals/managers.py
index c11150eb4c..c507088430 100644
--- a/pandas/core/internals/managers.py
+++ b/pandas/core/internals/managers.py
@@ -54,7 +54,11 @@ from pandas.core.dtypes.missing import (
 )
 
 import pandas.core.algorithms as algos
-from pandas.core.arrays import DatetimeArray
+from pandas.core.arrays import (
+    ArrowExtensionArray,
+    ArrowStringArray,
+    DatetimeArray,
+)
 from pandas.core.arrays._mixins import NDArrayBackedExtensionArray
 from pandas.core.construction import (
     ensure_wrapped_if_datetimelike,
@@ -72,6 +76,8 @@ from pandas.core.internals.base import (
     interleaved_dtype,
 )
 from pandas.core.internals.blocks import (
+    COW_WARNING_GENERAL_MSG,
+    COW_WARNING_SETITEM_MSG,
     Block,
     NumpyBlock,
     ensure_block_shape,
@@ -100,29 +106,6 @@ if TYPE_CHECKING:
     from pandas.api.extensions import ExtensionArray
 
 
-COW_WARNING_GENERAL_MSG = """\
-Setting a value on a view: behaviour will change in pandas 3.0.
-You are mutating a Series or DataFrame object, and currently this mutation will
-also have effect on other Series or DataFrame objects that share data with this
-object. In pandas 3.0 (with Copy-on-Write), updating one Series or DataFrame object
-will never modify another.
-"""
-
-
-COW_WARNING_SETITEM_MSG = """\
-Setting a value on a view: behaviour will change in pandas 3.0.
-Currently, the mutation will also have effect on the object that shares data
-with this object. For example, when setting a value in a Series that was
-extracted from a column of a DataFrame, that DataFrame will also be updated:
-
-    ser = df["col"]
-    ser[0] = 0     <--- in pandas 2, this also updates `df`
-
-In pandas 3.0 (with Copy-on-Write), updating one Series/DataFrame will never
-modify another, and thus in the example above, `df` will not be changed.
-"""
-
-
 class BaseBlockManager(DataManager):
     """
     Core internal data structure to implement DataFrame, Series, etc.
@@ -1343,11 +1326,15 @@ class BlockManager(libinternals.BlockManager, BaseBlockManager):
         intermediate Series at the DataFrame level (`s = df[loc]; s[idx] = value`)
         """
         if warn_copy_on_write() and not self._has_no_reference(loc):
-            warnings.warn(
-                COW_WARNING_GENERAL_MSG,
-                FutureWarning,
-                stacklevel=find_stack_level(),
-            )
+            if not isinstance(
+                self.blocks[self.blknos[loc]].values,
+                (ArrowExtensionArray, ArrowStringArray),
+            ):
+                warnings.warn(
+                    COW_WARNING_GENERAL_MSG,
+                    FutureWarning,
+                    stacklevel=find_stack_level(),
+                )
         elif using_copy_on_write() and not self._has_no_reference(loc):
             blkno = self.blknos[loc]
             # Split blocks to only copy the column we want to modify
diff --git a/pandas/core/reshape/pivot.py b/pandas/core/reshape/pivot.py
index eba4f72b5e..82718d4c43 100644
--- a/pandas/core/reshape/pivot.py
+++ b/pandas/core/reshape/pivot.py
@@ -10,6 +10,7 @@ from typing import (
     Literal,
     cast,
 )
+import warnings
 
 import numpy as np
 
@@ -18,6 +19,7 @@ from pandas.util._decorators import (
     Appender,
     Substitution,
 )
+from pandas.util._exceptions import find_stack_level
 
 from pandas.core.dtypes.cast import maybe_downcast_to_dtype
 from pandas.core.dtypes.common import (
@@ -68,7 +70,7 @@ def pivot_table(
     margins: bool = False,
     dropna: bool = True,
     margins_name: Hashable = "All",
-    observed: bool = False,
+    observed: bool | lib.NoDefault = lib.no_default,
     sort: bool = True,
 ) -> DataFrame:
     index = _convert_by(index)
@@ -123,7 +125,7 @@ def __internal_pivot_table(
     margins: bool,
     dropna: bool,
     margins_name: Hashable,
-    observed: bool,
+    observed: bool | lib.NoDefault,
     sort: bool,
 ) -> DataFrame:
     """
@@ -166,7 +168,18 @@ def __internal_pivot_table(
                 pass
         values = list(values)
 
-    grouped = data.groupby(keys, observed=observed, sort=sort, dropna=dropna)
+    observed_bool = False if observed is lib.no_default else observed
+    grouped = data.groupby(keys, observed=observed_bool, sort=sort, dropna=dropna)
+    if observed is lib.no_default and any(
+        ping._passed_categorical for ping in grouped.grouper.groupings
+    ):
+        warnings.warn(
+            "The default value of observed=False is deprecated and will change "
+            "to observed=True in a future version of pandas. Specify "
+            "observed=False to silence this warning and retain the current behavior",
+            category=FutureWarning,
+            stacklevel=find_stack_level(),
+        )
     agged = grouped.agg(aggfunc)
 
     if dropna and isinstance(agged, ABCDataFrame) and len(agged.columns):
@@ -719,6 +732,7 @@ def crosstab(
         margins=margins,
         margins_name=margins_name,
         dropna=dropna,
+        observed=False,
         **kwargs,  # type: ignore[arg-type]
     )
 
diff --git a/pandas/core/series.py b/pandas/core/series.py
index 6da0351766..464e066b4e 100644
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -1065,6 +1065,8 @@ class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
         key = com.apply_if_callable(key, self)
 
         if key is Ellipsis:
+            if using_copy_on_write() or warn_copy_on_write():
+                return self.copy(deep=False)
             return self
 
         key_is_scalar = is_scalar(key)
@@ -1175,7 +1177,7 @@ class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
         # If key is contained, would have returned by now
         indexer, new_index = self.index.get_loc_level(key)
         new_ser = self._constructor(self._values[indexer], index=new_index, copy=False)
-        if using_copy_on_write() and isinstance(indexer, slice):
+        if isinstance(indexer, slice):
             new_ser._mgr.add_references(self._mgr)  # type: ignore[arg-type]
         return new_ser.__finalize__(self)
 
@@ -1217,7 +1219,7 @@ class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
             new_ser = self._constructor(
                 new_values, index=new_index, name=self.name, copy=False
             )
-            if using_copy_on_write() and isinstance(loc, slice):
+            if isinstance(loc, slice):
                 new_ser._mgr.add_references(self._mgr)  # type: ignore[arg-type]
             return new_ser.__finalize__(self)
 
@@ -1320,7 +1322,7 @@ class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
                 # otherwise with listlike other we interpret series[mask] = other
                 #  as series[mask] = other[mask]
                 try:
-                    self._where(~key, value, inplace=True)
+                    self._where(~key, value, inplace=True, warn=warn)
                 except InvalidIndexError:
                     # test_where_dups
                     self.iloc[key] = value
@@ -3580,7 +3582,7 @@ class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
                     ChainedAssignmentError,
                     stacklevel=2,
                 )
-        elif not PYPY and not using_copy_on_write():
+        elif not PYPY and not using_copy_on_write() and self._is_view_after_cow_rules():
             ctr = sys.getrefcount(self)
             ref_count = REF_COUNT
             if _check_cacher(self):


Test patch:
diff --git a/pandas/tests/computation/test_eval.py b/pandas/tests/computation/test_eval.py
index 304fe82468..75473b8c50 100644
--- a/pandas/tests/computation/test_eval.py
+++ b/pandas/tests/computation/test_eval.py
@@ -1173,9 +1173,7 @@ class TestOperations:
         df.eval("c = a + b", inplace=True)
         tm.assert_frame_equal(df, expected)
 
-    # TODO(CoW-warn) this should not warn (DataFrame.eval creates refs to self)
-    @pytest.mark.filterwarnings("ignore:Setting a value on a view:FutureWarning")
-    def test_assignment_single_assign_local_overlap(self, warn_copy_on_write):
+    def test_assignment_single_assign_local_overlap(self):
         df = DataFrame(
             np.random.default_rng(2).standard_normal((5, 2)), columns=list("ab")
         )
@@ -1229,8 +1227,6 @@ class TestOperations:
         tm.assert_series_equal(result, expected, check_names=False)
 
     @pytest.mark.xfail(reason="Unknown: Omitted test_ in name prior.")
-    # TODO(CoW-warn) this should not warn (DataFrame.eval creates refs to self)
-    @pytest.mark.filterwarnings("ignore:Setting a value on a view:FutureWarning")
     def test_assignment_not_inplace(self):
         # see gh-9297
         df = DataFrame(
@@ -1244,7 +1240,7 @@ class TestOperations:
         expected["c"] = expected["a"] + expected["b"]
         tm.assert_frame_equal(df, expected)
 
-    def test_multi_line_expression(self):
+    def test_multi_line_expression(self, warn_copy_on_write):
         # GH 11149
         df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
         expected = df.copy()
@@ -1917,8 +1913,8 @@ def test_set_inplace(using_copy_on_write, warn_copy_on_write):
     df = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6], "C": [7, 8, 9]})
     result_view = df[:]
     ser = df["A"]
-    # with tm.assert_cow_warning(warn_copy_on_write):
-    df.eval("A = B + C", inplace=True)
+    with tm.assert_cow_warning(warn_copy_on_write):
+        df.eval("A = B + C", inplace=True)
     expected = DataFrame({"A": [11, 13, 15], "B": [4, 5, 6], "C": [7, 8, 9]})
     tm.assert_frame_equal(df, expected)
     if not using_copy_on_write:
diff --git a/pandas/tests/copy_view/test_chained_assignment_deprecation.py b/pandas/tests/copy_view/test_chained_assignment_deprecation.py
index 7b08d9b80f..b51a592091 100644
--- a/pandas/tests/copy_view/test_chained_assignment_deprecation.py
+++ b/pandas/tests/copy_view/test_chained_assignment_deprecation.py
@@ -1,9 +1,15 @@
 import numpy as np
 import pytest
 
-from pandas.errors import ChainedAssignmentError
+from pandas.errors import (
+    ChainedAssignmentError,
+    SettingWithCopyWarning,
+)
 
-from pandas import DataFrame
+from pandas import (
+    DataFrame,
+    option_context,
+)
 import pandas._testing as tm
 
 
@@ -70,7 +76,7 @@ def test_methods_iloc_getitem_item_cache(func, args, using_copy_on_write):
 @pytest.mark.parametrize(
     "indexer", [0, [0, 1], slice(0, 2), np.array([True, False, True])]
 )
-def test_series_setitem(indexer, using_copy_on_write):
+def test_series_setitem(indexer, using_copy_on_write, warn_copy_on_write):
     # ensure we only get a single warning for those typical cases of chained
     # assignment
     df = DataFrame({"a": [1, 2, 3], "b": 1})
@@ -85,3 +91,17 @@ def test_series_setitem(indexer, using_copy_on_write):
     else:
         assert record[0].category == FutureWarning
         assert "ChainedAssignmentError" in record[0].message.args[0]
+
+
+@pytest.mark.filterwarnings("ignore::pandas.errors.SettingWithCopyWarning")
+@pytest.mark.parametrize(
+    "indexer", ["a", ["a", "b"], slice(0, 2), np.array([True, False, True])]
+)
+def test_frame_setitem(indexer, using_copy_on_write):
+    df = DataFrame({"a": [1, 2, 3, 4, 5], "b": 1})
+
+    extra_warnings = () if using_copy_on_write else (SettingWithCopyWarning,)
+
+    with option_context("chained_assignment", "warn"):
+        with tm.raises_chained_assignment_error(extra_warnings=extra_warnings):
+            df[0:3][indexer] = 10
diff --git a/pandas/tests/copy_view/test_clip.py b/pandas/tests/copy_view/test_clip.py
index 13ddd479c7..7c87646424 100644
--- a/pandas/tests/copy_view/test_clip.py
+++ b/pandas/tests/copy_view/test_clip.py
@@ -8,12 +8,16 @@ import pandas._testing as tm
 from pandas.tests.copy_view.util import get_array
 
 
-def test_clip_inplace_reference(using_copy_on_write):
+def test_clip_inplace_reference(using_copy_on_write, warn_copy_on_write):
     df = DataFrame({"a": [1.5, 2, 3]})
     df_copy = df.copy()
     arr_a = get_array(df, "a")
     view = df[:]
-    df.clip(lower=2, inplace=True)
+    if warn_copy_on_write:
+        with tm.assert_cow_warning():
+            df.clip(lower=2, inplace=True)
+    else:
+        df.clip(lower=2, inplace=True)
 
     if using_copy_on_write:
         assert not np.shares_memory(get_array(df, "a"), arr_a)
@@ -88,10 +92,10 @@ def test_clip_chained_inplace(using_copy_on_write):
         with tm.assert_produces_warning(FutureWarning, match="inplace method"):
             df["a"].clip(1, 2, inplace=True)
 
-        with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+        with tm.assert_produces_warning(None):
             with option_context("mode.chained_assignment", None):
                 df[["a"]].clip(1, 2, inplace=True)
 
-        with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+        with tm.assert_produces_warning(None):
             with option_context("mode.chained_assignment", None):
                 df[df["a"] > 1].clip(1, 2, inplace=True)
diff --git a/pandas/tests/copy_view/test_indexing.py b/pandas/tests/copy_view/test_indexing.py
index 72b7aea370..33b8ce218f 100644
--- a/pandas/tests/copy_view/test_indexing.py
+++ b/pandas/tests/copy_view/test_indexing.py
@@ -367,10 +367,11 @@ def test_subset_set_with_mask(backend, using_copy_on_write, warn_copy_on_write):
 
     mask = subset > 3
 
-    # TODO(CoW-warn) should warn -> mask is a DataFrame, which ends up going through
-    # DataFrame._where(..., inplace=True)
-    if using_copy_on_write or warn_copy_on_write:
+    if using_copy_on_write:
         subset[mask] = 0
+    elif warn_copy_on_write:
+        with tm.assert_cow_warning():
+            subset[mask] = 0
     else:
         with pd.option_context("chained_assignment", "warn"):
             with tm.assert_produces_warning(SettingWithCopyWarning):
@@ -845,6 +846,31 @@ def test_series_getitem_slice(backend, using_copy_on_write, warn_copy_on_write):
         assert s.iloc[0] == 0
 
 
+def test_series_getitem_ellipsis(using_copy_on_write, warn_copy_on_write):
+    # Case: taking a view of a Series using Ellipsis + afterwards modifying the subset
+    s = Series([1, 2, 3])
+    s_orig = s.copy()
+
+    subset = s[...]
+    assert np.shares_memory(get_array(subset), get_array(s))
+
+    with tm.assert_cow_warning(warn_copy_on_write):
+        subset.iloc[0] = 0
+
+    if using_copy_on_write:
+        assert not np.shares_memory(get_array(subset), get_array(s))
+
+    expected = Series([0, 2, 3])
+    tm.assert_series_equal(subset, expected)
+
+    if using_copy_on_write:
+        # original parent series is not modified (CoW)
+        tm.assert_series_equal(s, s_orig)
+    else:
+        # original parent series is actually updated
+        assert s.iloc[0] == 0
+
+
 @pytest.mark.parametrize(
     "indexer",
     [slice(0, 2), np.array([True, True, False]), np.array([0, 1])],
@@ -867,18 +893,8 @@ def test_series_subset_set_with_indexer(
         and indexer.dtype.kind == "i"
     ):
         warn = FutureWarning
-    is_mask = (
-        indexer_si is tm.setitem
-        and isinstance(indexer, np.ndarray)
-        and indexer.dtype.kind == "b"
-    )
     if warn_copy_on_write:
-        # TODO(CoW-warn) should also warn for setting with mask
-        # -> Series.__setitem__ with boolean mask ends up using Series._set_values
-        # or Series._where depending on value being set
-        with tm.assert_cow_warning(
-            not is_mask, raise_on_extra_warnings=warn is not None
-        ):
+        with tm.assert_cow_warning(raise_on_extra_warnings=warn is not None):
             indexer_si(subset)[indexer] = 0
     else:
         with tm.assert_produces_warning(warn, match=msg):
@@ -1150,16 +1166,18 @@ def test_set_value_copy_only_necessary_column(
             assert np.shares_memory(get_array(df, "a"), get_array(view, "a"))
 
 
-def test_series_midx_slice(using_copy_on_write):
+def test_series_midx_slice(using_copy_on_write, warn_copy_on_write):
     ser = Series([1, 2, 3], index=pd.MultiIndex.from_arrays([[1, 1, 2], [3, 4, 5]]))
+    ser_orig = ser.copy()
     result = ser[1]
     assert np.shares_memory(get_array(ser), get_array(result))
-    # TODO(CoW-warn) should warn -> reference is only tracked in CoW mode, so
-    # warning is not triggered
-    result.iloc[0] = 100
+    with tm.assert_cow_warning(warn_copy_on_write):
+        result.iloc[0] = 100
     if using_copy_on_write:
+        tm.assert_series_equal(ser, ser_orig)
+    else:
         expected = Series(
-            [1, 2, 3], index=pd.MultiIndex.from_arrays([[1, 1, 2], [3, 4, 5]])
+            [100, 2, 3], index=pd.MultiIndex.from_arrays([[1, 1, 2], [3, 4, 5]])
         )
         tm.assert_series_equal(ser, expected)
 
@@ -1190,16 +1208,15 @@ def test_getitem_midx_slice(
         assert df.iloc[0, 0] == 100
 
 
-def test_series_midx_tuples_slice(using_copy_on_write):
+def test_series_midx_tuples_slice(using_copy_on_write, warn_copy_on_write):
     ser = Series(
         [1, 2, 3],
         index=pd.MultiIndex.from_tuples([((1, 2), 3), ((1, 2), 4), ((2, 3), 4)]),
     )
     result = ser[(1, 2)]
     assert np.shares_memory(get_array(ser), get_array(result))
-    # TODO(CoW-warn) should warn -> reference is only tracked in CoW mode, so
-    # warning is not triggered
-    result.iloc[0] = 100
+    with tm.assert_cow_warning(warn_copy_on_write):
+        result.iloc[0] = 100
     if using_copy_on_write:
         expected = Series(
             [1, 2, 3],
diff --git a/pandas/tests/copy_view/test_interp_fillna.py b/pandas/tests/copy_view/test_interp_fillna.py
index cdb06de90a..04708122a8 100644
--- a/pandas/tests/copy_view/test_interp_fillna.py
+++ b/pandas/tests/copy_view/test_interp_fillna.py
@@ -344,8 +344,9 @@ def test_fillna_inplace_ea_noop_shares_memory(
         assert not df._mgr._has_no_reference(1)
         assert not view._mgr._has_no_reference(1)
 
-    # TODO(CoW-warn) should this warn for ArrowDtype?
-    with tm.assert_cow_warning(warn_copy_on_write):
+    with tm.assert_cow_warning(
+        warn_copy_on_write and "pyarrow" not in any_numeric_ea_and_arrow_dtype
+    ):
         df.iloc[0, 1] = 100
     if isinstance(df["a"].dtype, ArrowDtype) or using_copy_on_write:
         tm.assert_frame_equal(df_orig, view)
@@ -366,11 +367,11 @@ def test_fillna_chained_assignment(using_copy_on_write):
             df[["a"]].fillna(100, inplace=True)
         tm.assert_frame_equal(df, df_orig)
     else:
-        with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+        with tm.assert_produces_warning(None):
             with option_context("mode.chained_assignment", None):
                 df[["a"]].fillna(100, inplace=True)
 
-        with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+        with tm.assert_produces_warning(None):
             with option_context("mode.chained_assignment", None):
                 df[df.a > 5].fillna(100, inplace=True)
 
@@ -394,10 +395,10 @@ def test_interpolate_chained_assignment(using_copy_on_write, func):
         with tm.assert_produces_warning(FutureWarning, match="inplace method"):
             getattr(df["a"], func)(inplace=True)
 
-        with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+        with tm.assert_produces_warning(None):
             with option_context("mode.chained_assignment", None):
                 getattr(df[["a"]], func)(inplace=True)
 
-        with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+        with tm.assert_produces_warning(None):
             with option_context("mode.chained_assignment", None):
                 getattr(df[df["a"] > 1], func)(inplace=True)
diff --git a/pandas/tests/copy_view/test_methods.py b/pandas/tests/copy_view/test_methods.py
index 806842dcab..62214293df 100644
--- a/pandas/tests/copy_view/test_methods.py
+++ b/pandas/tests/copy_view/test_methods.py
@@ -1407,11 +1407,12 @@ def test_items(using_copy_on_write, warn_copy_on_write):
 
 
 @pytest.mark.parametrize("dtype", ["int64", "Int64"])
-def test_putmask(using_copy_on_write, dtype):
+def test_putmask(using_copy_on_write, dtype, warn_copy_on_write):
     df = DataFrame({"a": [1, 2], "b": 1, "c": 2}, dtype=dtype)
     view = df[:]
     df_orig = df.copy()
-    df[df == df] = 5
+    with tm.assert_cow_warning(warn_copy_on_write):
+        df[df == df] = 5
 
     if using_copy_on_write:
         assert not np.shares_memory(get_array(view, "a"), get_array(df, "a"))
@@ -1445,15 +1446,21 @@ def test_putmask_aligns_rhs_no_reference(using_copy_on_write, dtype):
 @pytest.mark.parametrize(
     "val, exp, warn", [(5.5, True, FutureWarning), (5, False, None)]
 )
-def test_putmask_dont_copy_some_blocks(using_copy_on_write, val, exp, warn):
+def test_putmask_dont_copy_some_blocks(
+    using_copy_on_write, val, exp, warn, warn_copy_on_write
+):
     df = DataFrame({"a": [1, 2], "b": 1, "c": 1.5})
     view = df[:]
     df_orig = df.copy()
     indexer = DataFrame(
         [[True, False, False], [True, False, False]], columns=list("abc")
     )
-    with tm.assert_produces_warning(warn, match="incompatible dtype"):
-        df[indexer] = val
+    if warn_copy_on_write:
+        with tm.assert_cow_warning():
+            df[indexer] = val
+    else:
+        with tm.assert_produces_warning(warn, match="incompatible dtype"):
+            df[indexer] = val
 
     if using_copy_on_write:
         assert not np.shares_memory(get_array(view, "a"), get_array(df, "a"))
@@ -1555,11 +1562,11 @@ def test_chained_where_mask(using_copy_on_write, func):
         with tm.assert_produces_warning(FutureWarning, match="inplace method"):
             getattr(df["a"], func)(df["a"] > 2, 5, inplace=True)
 
-        with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+        with tm.assert_produces_warning(None):
             with option_context("mode.chained_assignment", None):
                 getattr(df[["a"]], func)(df["a"] > 2, 5, inplace=True)
 
-        with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+        with tm.assert_produces_warning(None):
             with option_context("mode.chained_assignment", None):
                 getattr(df[df["a"] > 1], func)(df["a"] > 2, 5, inplace=True)
 
@@ -1796,13 +1803,17 @@ def test_update_frame(using_copy_on_write, warn_copy_on_write):
         tm.assert_frame_equal(view, expected)
 
 
-def test_update_series(using_copy_on_write):
+def test_update_series(using_copy_on_write, warn_copy_on_write):
     ser1 = Series([1.0, 2.0, 3.0])
     ser2 = Series([100.0], index=[1])
     ser1_orig = ser1.copy()
     view = ser1[:]
 
-    ser1.update(ser2)
+    if warn_copy_on_write:
+        with tm.assert_cow_warning():
+            ser1.update(ser2)
+    else:
+        ser1.update(ser2)
 
     expected = Series([1.0, 100.0, 3.0])
     tm.assert_series_equal(ser1, expected)
@@ -1829,11 +1840,11 @@ def test_update_chained_assignment(using_copy_on_write):
         with tm.assert_produces_warning(FutureWarning, match="inplace method"):
             df["a"].update(ser2)
 
-        with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+        with tm.assert_produces_warning(None):
             with option_context("mode.chained_assignment", None):
                 df[["a"]].update(ser2.to_frame())
 
-        with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+        with tm.assert_produces_warning(None):
             with option_context("mode.chained_assignment", None):
                 df[df["a"] > 1].update(ser2.to_frame())
 
diff --git a/pandas/tests/copy_view/test_replace.py b/pandas/tests/copy_view/test_replace.py
index d11a2893be..eb3b1a5ef6 100644
--- a/pandas/tests/copy_view/test_replace.py
+++ b/pandas/tests/copy_view/test_replace.py
@@ -279,14 +279,18 @@ def test_replace_categorical(using_copy_on_write, val):
 
 
 @pytest.mark.parametrize("method", ["where", "mask"])
-def test_masking_inplace(using_copy_on_write, method):
+def test_masking_inplace(using_copy_on_write, method, warn_copy_on_write):
     df = DataFrame({"a": [1.5, 2, 3]})
     df_orig = df.copy()
     arr_a = get_array(df, "a")
     view = df[:]
 
     method = getattr(df, method)
-    method(df["a"] > 1.6, -1, inplace=True)
+    if warn_copy_on_write:
+        with tm.assert_cow_warning():
+            method(df["a"] > 1.6, -1, inplace=True)
+    else:
+        method(df["a"] > 1.6, -1, inplace=True)
 
     if using_copy_on_write:
         assert not np.shares_memory(get_array(df, "a"), arr_a)
@@ -397,11 +401,11 @@ def test_replace_chained_assignment(using_copy_on_write):
             df[["a"]].replace(1, 100, inplace=True)
         tm.assert_frame_equal(df, df_orig)
     else:
-        with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+        with tm.assert_produces_warning(None):
             with option_context("mode.chained_assignment", None):
                 df[["a"]].replace(1, 100, inplace=True)
 
-        with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+        with tm.assert_produces_warning(None):
             with option_context("mode.chained_assignment", None):
                 df[df.a > 5].replace(1, 100, inplace=True)
 
diff --git a/pandas/tests/frame/methods/test_quantile.py b/pandas/tests/frame/methods/test_quantile.py
index 787b77a5c7..0f27eae1a3 100644
--- a/pandas/tests/frame/methods/test_quantile.py
+++ b/pandas/tests/frame/methods/test_quantile.py
@@ -110,8 +110,6 @@ class TestDataFrameQuantile:
             request.applymarker(pytest.mark.xfail(reason="Axis name incorrectly set."))
         tm.assert_series_equal(rs, xp)
 
-    # TODO(CoW-warn) should not need to warn
-    @pytest.mark.filterwarnings("ignore:Setting a value on a view:FutureWarning")
     def test_axis(self, interp_method, request, using_array_manager):
         # axis
         interpolation, method = interp_method
diff --git a/pandas/tests/frame/methods/test_replace.py b/pandas/tests/frame/methods/test_replace.py
index 4b32d3de59..13e2c1a249 100644
--- a/pandas/tests/frame/methods/test_replace.py
+++ b/pandas/tests/frame/methods/test_replace.py
@@ -729,11 +729,7 @@ class TestDataFrameReplace:
 
         tsframe.loc[tsframe.index[:5], "A"] = np.nan
         tsframe.loc[tsframe.index[-5:], "A"] = np.nan
-        tsframe.loc[tsframe.index[:5], "B"] = -1e8
-
-        b = tsframe["B"]
-        b[b == -1e8] = np.nan
-        tsframe["B"] = b
+        tsframe.loc[tsframe.index[:5], "B"] = np.nan
         msg = "DataFrame.fillna with 'method' is deprecated"
         with tm.assert_produces_warning(FutureWarning, match=msg):
             # TODO: what is this even testing?
diff --git a/pandas/tests/groupby/test_groupby.py b/pandas/tests/groupby/test_groupby.py
index 254a12d9bd..398cbc7863 100644
--- a/pandas/tests/groupby/test_groupby.py
+++ b/pandas/tests/groupby/test_groupby.py
@@ -41,8 +41,6 @@ def test_repr():
     assert result == expected
 
 
-# TODO(CoW-warn) this should NOT warn -> inplace operator triggers it
-@pytest.mark.filterwarnings("ignore:Setting a value on a view:FutureWarning")
 def test_groupby_std_datetimelike(warn_copy_on_write):
     # GH#48481
     tdi = pd.timedelta_range("1 Day", periods=10000)
diff --git a/pandas/tests/indexes/datetimes/methods/test_to_period.py b/pandas/tests/indexes/datetimes/methods/test_to_period.py
index aa217e895c..2c68ddd3d3 100644
--- a/pandas/tests/indexes/datetimes/methods/test_to_period.py
+++ b/pandas/tests/indexes/datetimes/methods/test_to_period.py
@@ -230,3 +230,11 @@ class TestToPeriod:
         idx = DatetimeIndex(["2000-01-01", "2000-01-02", "2000-01-03"])
         assert idx.freqstr is None
         tm.assert_index_equal(idx.to_period(), expected)
+
+    @pytest.mark.parametrize("freq", ["2BMS", "1SME-15"])
+    def test_to_period_offsets_not_supported(self, freq):
+        # GH#56243
+        msg = f"{freq[1:]} is not supported as period frequency"
+        ts = date_range("1/1/2012", periods=4, freq=freq)
+        with pytest.raises(TypeError, match=msg):
+            ts.to_period()
diff --git a/pandas/tests/indexing/multiindex/test_chaining_and_caching.py b/pandas/tests/indexing/multiindex/test_chaining_and_caching.py
index 01e5db87ce..0dd1a56890 100644
--- a/pandas/tests/indexing/multiindex/test_chaining_and_caching.py
+++ b/pandas/tests/indexing/multiindex/test_chaining_and_caching.py
@@ -34,12 +34,12 @@ def test_detect_chained_assignment(using_copy_on_write, warn_copy_on_write):
         with tm.raises_chained_assignment_error():
             zed["eyes"]["right"].fillna(value=555, inplace=True)
     elif warn_copy_on_write:
-        with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+        with tm.assert_produces_warning(None):
             zed["eyes"]["right"].fillna(value=555, inplace=True)
     else:
         msg = "A value is trying to be set on a copy of a slice from a DataFrame"
         with pytest.raises(SettingWithCopyError, match=msg):
-            with tm.assert_produces_warning(FutureWarning, match="inplace method"):
+            with tm.assert_produces_warning(None):
                 zed["eyes"]["right"].fillna(value=555, inplace=True)
 
 
diff --git a/pandas/tests/indexing/multiindex/test_setitem.py b/pandas/tests/indexing/multiindex/test_setitem.py
index e613f1102b..53ad4d6b41 100644
--- a/pandas/tests/indexing/multiindex/test_setitem.py
+++ b/pandas/tests/indexing/multiindex/test_setitem.py
@@ -548,7 +548,8 @@ def test_frame_setitem_copy_raises(
     else:
         msg = "A value is trying to be set on a copy of a slice from a DataFrame"
         with pytest.raises(SettingWithCopyError, match=msg):
-            df["foo"]["one"] = 2
+            with tm.raises_chained_assignment_error():
+                df["foo"]["one"] = 2
 
 
 def test_frame_setitem_copy_no_write(
@@ -563,7 +564,8 @@ def test_frame_setitem_copy_no_write(
     else:
         msg = "A value is trying to be set on a copy of a slice from a DataFrame"
         with pytest.raises(SettingWithCopyError, match=msg):
-            df["foo"]["one"] = 2
+            with tm.raises_chained_assignment_error():
+                df["foo"]["one"] = 2
 
     result = df
     tm.assert_frame_equal(result, expected)
diff --git a/pandas/tests/indexing/test_chaining_and_caching.py b/pandas/tests/indexing/test_chaining_and_caching.py
index 230c575e6e..b97df376ac 100644
--- a/pandas/tests/indexing/test_chaining_and_caching.py
+++ b/pandas/tests/indexing/test_chaining_and_caching.py
@@ -112,11 +112,7 @@ class TestCaching:
 
         out = DataFrame({"A": [0, 0, 0]}, index=date_range("5/7/2014", "5/9/2014"))
         for ix, row in df.iterrows():
-            # TODO(CoW-warn) should not warn
-            with tm.assert_produces_warning(
-                FutureWarning if warn_copy_on_write else None
-            ):
-                out.loc[six:eix, row["C"]] += row["D"]
+            out.loc[six:eix, row["C"]] += row["D"]
 
         tm.assert_frame_equal(out, expected)
         tm.assert_series_equal(out["A"], expected["A"])
@@ -452,7 +448,8 @@ class TestChaining:
                 df.iloc[0:5]["group"] = "a"
         else:
             with pytest.raises(SettingWithCopyError, match=msg):
-                df.iloc[0:5]["group"] = "a"
+                with tm.raises_chained_assignment_error():
+                    df.iloc[0:5]["group"] = "a"
 
     @pytest.mark.arm_slow
     def test_detect_chained_assignment_changing_dtype(
diff --git a/pandas/tests/indexing/test_iloc.py b/pandas/tests/indexing/test_iloc.py
index baf2cdae43..31263b44ed 100644
--- a/pandas/tests/indexing/test_iloc.py
+++ b/pandas/tests/indexing/test_iloc.py
@@ -426,9 +426,7 @@ class TestiLocBaseIndependent:
         tm.assert_frame_equal(df.iloc[10:, :2], df2)
         tm.assert_frame_equal(df.iloc[10:, 2:], df1)
 
-    # TODO(CoW-warn) this should NOT warn -> Series inplace operator
-    @pytest.mark.filterwarnings("ignore:Setting a value on a view:FutureWarning")
-    def test_iloc_setitem(self):
+    def test_iloc_setitem(self, warn_copy_on_write):
         df = DataFrame(
             np.random.default_rng(2).standard_normal((4, 4)),
             index=np.arange(0, 8, 2),
@@ -1147,7 +1145,7 @@ class TestiLocBaseIndependent:
         expected = df.take([0], axis=1)
         tm.assert_frame_equal(result, expected)
 
-    def test_iloc_interval(self, warn_copy_on_write):
+    def test_iloc_interval(self):
         # GH#17130
         df = DataFrame({Interval(1, 2): [1, 2]})
 
@@ -1160,9 +1158,7 @@ class TestiLocBaseIndependent:
         tm.assert_series_equal(result, expected)
 
         result = df.copy()
-        # TODO(CoW-warn) false positive
-        with tm.assert_cow_warning(warn_copy_on_write):
-            result.iloc[:, 0] += 1
+        result.iloc[:, 0] += 1
         expected = DataFrame({Interval(1, 2): [2, 3]})
         tm.assert_frame_equal(result, expected)
 
diff --git a/pandas/tests/reshape/test_pivot.py b/pandas/tests/reshape/test_pivot.py
index 4a852daaad..dab2b034d3 100644
--- a/pandas/tests/reshape/test_pivot.py
+++ b/pandas/tests/reshape/test_pivot.py
@@ -201,7 +201,9 @@ class TestPivotTable:
             ["c", "d", "c", "d"], categories=["c", "d", "y"], ordered=True
         )
         df = DataFrame({"A": cat1, "B": cat2, "values": [1, 2, 3, 4]})
-        result = pivot_table(df, values="values", index=["A", "B"], dropna=True)
+        msg = "The default value of observed=False is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = pivot_table(df, values="values", index=["A", "B"], dropna=True)
 
         exp_index = MultiIndex.from_arrays([cat1, cat2], names=["A", "B"])
         expected = DataFrame({"values": [1.0, 2.0, 3.0, 4.0]}, index=exp_index)
@@ -220,7 +222,9 @@ class TestPivotTable:
         )
 
         df["A"] = df["A"].astype(CategoricalDtype(categories, ordered=False))
-        result = df.pivot_table(index="B", columns="A", values="C", dropna=dropna)
+        msg = "The default value of observed=False is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = df.pivot_table(index="B", columns="A", values="C", dropna=dropna)
         expected_columns = Series(["a", "b", "c"], name="A")
         expected_columns = expected_columns.astype(
             CategoricalDtype(categories, ordered=False)
@@ -250,7 +254,9 @@ class TestPivotTable:
             }
         )
 
-        result = df.pivot_table(index="A", values="B", dropna=dropna)
+        msg = "The default value of observed=False is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = df.pivot_table(index="A", values="B", dropna=dropna)
         if dropna:
             values = [2.0, 3.0]
             codes = [0, 1]
@@ -283,7 +289,9 @@ class TestPivotTable:
             }
         )
 
-        result = df.pivot_table(index="A", values="B", dropna=dropna)
+        msg = "The default value of observed=False is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = df.pivot_table(index="A", values="B", dropna=dropna)
         expected = DataFrame(
             {"B": [2.0, 3.0, 0.0]},
             index=Index(
@@ -301,7 +309,10 @@ class TestPivotTable:
     def test_pivot_with_interval_index(self, interval_values, dropna):
         # GH 25814
         df = DataFrame({"A": interval_values, "B": 1})
-        result = df.pivot_table(index="A", values="B", dropna=dropna)
+
+        msg = "The default value of observed=False is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = df.pivot_table(index="A", values="B", dropna=dropna)
         expected = DataFrame(
             {"B": 1.0}, index=Index(interval_values.unique(), name="A")
         )
@@ -322,9 +333,11 @@ class TestPivotTable:
             }
         )
 
-        pivot_tab = pivot_table(
-            df, index="C", columns="B", values="A", aggfunc="sum", margins=True
-        )
+        msg = "The default value of observed=False is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            pivot_tab = pivot_table(
+                df, index="C", columns="B", values="A", aggfunc="sum", margins=True
+            )
 
         result = pivot_tab["All"]
         expected = Series(
@@ -1827,7 +1840,9 @@ class TestPivotTable:
 
         df.y = df.y.astype("category")
         df.z = df.z.astype("category")
-        table = df.pivot_table("x", "y", "z", dropna=observed, margins=True)
+        msg = "The default value of observed=False is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            table = df.pivot_table("x", "y", "z", dropna=observed, margins=True)
         tm.assert_frame_equal(table, expected)
 
     def test_margins_casted_to_float(self):
@@ -1889,9 +1904,11 @@ class TestPivotTable:
             {"C1": ["A", "B", "C", "C"], "C2": ["a", "a", "b", "b"], "V": [1, 2, 3, 4]}
         )
         df["C1"] = df["C1"].astype("category")
-        result = df.pivot_table(
-            "V", index="C1", columns="C2", dropna=observed, aggfunc="count"
-        )
+        msg = "The default value of observed=False is deprecated"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            result = df.pivot_table(
+                "V", index="C1", columns="C2", dropna=observed, aggfunc="count"
+            )
 
         expected_index = pd.CategoricalIndex(
             ["A", "B", "C"], categories=["A", "B", "C"], ordered=False, name="C1"
diff --git a/pandas/tests/series/indexing/test_indexing.py b/pandas/tests/series/indexing/test_indexing.py
index 0c788b371a..c52e47a812 100644
--- a/pandas/tests/series/indexing/test_indexing.py
+++ b/pandas/tests/series/indexing/test_indexing.py
@@ -101,14 +101,16 @@ def test_basic_getitem_dt64tz_values():
     assert result == expected
 
 
-def test_getitem_setitem_ellipsis():
+def test_getitem_setitem_ellipsis(using_copy_on_write, warn_copy_on_write):
     s = Series(np.random.default_rng(2).standard_normal(10))
 
     result = s[...]
     tm.assert_series_equal(result, s)
 
-    s[...] = 5
-    assert (result == 5).all()
+    with tm.assert_cow_warning(warn_copy_on_write):
+        s[...] = 5
+    if not using_copy_on_write:
+        assert (result == 5).all()
 
 
 @pytest.mark.parametrize(