Repository: pandas-dev/pandas

Issue:
DEPR: is_copy
this has always been an internal attribute. We can simply replace by ``._is_copy`` and provide a deprecation warning on the property.


Discussion:
`is_copy` is used in the most popular google hits about SettingWithCopyWarning, eg https://stackoverflow.com/questions/20625582/how-to-deal-with-settingwithcopywarning-in-pandas and https://www.dataquest.io/blog/settingwithcopywarning/ 
(actually in the first you are using it yourself in an answer :-))

What is the alternative for power users in library code if they want to avoid an extra unnecessary copy?

(I never use it myself, as I just do `copy()` to avoid if needed, so I am personally not attached to this method,  but typically don't handle with data where doing an extra `copy()` is a problem. But I think the use case mentioned by Andreas in https://github.com/pandas-dev/pandas/issues/18799 is a valid one)


Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -108,7 +108,7 @@ class NDFrame(PandasObject, SelectionMixin):
     axes : list
     copy : boolean, default False
     """
-    _internal_names = ['_data', '_cacher', '_item_cache', '_cache', 'is_copy',
+    _internal_names = ['_data', '_cacher', '_item_cache', '_cache', '_is_copy',
                        '_subtyp', '_name', '_index', '_default_kind',
                        '_default_fill_value', '_metadata', '__array_struct__',
                        '__array_interface__']
@@ -117,7 +117,7 @@ class NDFrame(PandasObject, SelectionMixin):
     _deprecations = frozenset(['as_blocks', 'blocks',
                                'consolidate', 'convert_objects'])
     _metadata = []
-    is_copy = None
+    _is_copy = None
 
     def __init__(self, data, axes=None, copy=False, dtype=None,
                  fastpath=False):
@@ -132,10 +132,22 @@ def __init__(self, data, axes=None, copy=False, dtype=None,
                 for i, ax in enumerate(axes):
                     data = data.reindex_axis(ax, axis=i)
 
-        object.__setattr__(self, 'is_copy', None)
+        object.__setattr__(self, '_is_copy', None)
         object.__setattr__(self, '_data', data)
         object.__setattr__(self, '_item_cache', {})
 
+    @property
+    def is_copy(self):
+        warnings.warn("Attribute 'is_copy' is deprecated and will be removed "
+                      "in a future version.", FutureWarning, stacklevel=2)
+        return self._is_copy
+
+    @is_copy.setter
+    def is_copy(self, msg):
+        warnings.warn("Attribute 'is_copy' is deprecated and will be removed "
+                      "in a future version.", FutureWarning, stacklevel=2)
+        self._is_copy = msg
+
     def _repr_data_resource_(self):
         """
         Not a real Jupyter special repr method, but we use the same
@@ -2153,7 +2165,7 @@ def _get_item_cache(self, item):
             res._set_as_cached(item, self)
 
             # for a chain
-            res.is_copy = self.is_copy
+            res._is_copy = self._is_copy
         return res
 
     def _set_as_cached(self, item, cacher):
@@ -2264,12 +2276,12 @@ def _set_item(self, key, value):
 
     def _set_is_copy(self, ref=None, copy=True):
         if not copy:
-            self.is_copy = None
+            self._is_copy = None
         else:
             if ref is not None:
-                self.is_copy = weakref.ref(ref)
+                self._is_copy = weakref.ref(ref)
             else:
-                self.is_copy = None
+                self._is_copy = None
 
     def _check_is_chained_assignment_possible(self):
         """
@@ -2288,7 +2300,7 @@ def _check_is_chained_assignment_possible(self):
                 self._check_setitem_copy(stacklevel=4, t='referant',
                                          force=True)
             return True
-        elif self.is_copy:
+        elif self._is_copy:
             self._check_setitem_copy(stacklevel=4, t='referant')
         return False
 
@@ -2323,7 +2335,7 @@ def _check_setitem_copy(self, stacklevel=4, t='setting', force=False):
 
         """
 
-        if force or self.is_copy:
+        if force or self._is_copy:
 
             value = config.get_option('mode.chained_assignment')
             if value is None:
@@ -2333,23 +2345,23 @@ def _check_setitem_copy(self, stacklevel=4, t='setting', force=False):
             # the copy weakref
             try:
                 gc.collect(2)
-                if not gc.get_referents(self.is_copy()):
-                    self.is_copy = None
+                if not gc.get_referents(self._is_copy()):
+                    self._is_copy = None
                     return
             except Exception:
                 pass
 
             # we might be a false positive
             try:
-                if self.is_copy().shape == self.shape:
-                    self.is_copy = None
+                if self._is_copy().shape == self.shape:
+                    self._is_copy = None
                     return
             except Exception:
                 pass
 
             # a custom message
-            if isinstance(self.is_copy, string_types):
-                t = self.is_copy
+            if isinstance(self._is_copy, string_types):
+                t = self._is_copy
 
             elif t == 'referant':
                 t = ("\n"
diff --git a/pandas/core/indexes/accessors.py b/pandas/core/indexes/accessors.py
--- a/pandas/core/indexes/accessors.py
+++ b/pandas/core/indexes/accessors.py
@@ -113,9 +113,9 @@ def _delegate_property_get(self, name):
         result = Series(result, index=self.index, name=self.name)
 
         # setting this object will show a SettingWithCopyWarning/Error
-        result.is_copy = ("modifications to a property of a datetimelike "
-                          "object are not supported and are discarded. "
-                          "Change values on the original.")
+        result._is_copy = ("modifications to a property of a datetimelike "
+                           "object are not supported and are discarded. "
+                           "Change values on the original.")
 
         return result
 
@@ -136,9 +136,9 @@ def _delegate_method(self, name, *args, **kwargs):
         result = Series(result, index=self.index, name=self.name)
 
         # setting this object will show a SettingWithCopyWarning/Error
-        result.is_copy = ("modifications to a method of a datetimelike object "
-                          "are not supported and are discarded. Change "
-                          "values on the original.")
+        result._is_copy = ("modifications to a method of a datetimelike "
+                           "object are not supported and are discarded. "
+                           "Change values on the original.")
 
         return result
 
diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py
--- a/pandas/core/indexing.py
+++ b/pandas/core/indexing.py
@@ -366,7 +366,7 @@ def _setitem_with_indexer(self, indexer, value):
                     labels = index.insert(len(index), key)
                     self.obj._data = self.obj.reindex(labels, axis=i)._data
                     self.obj._maybe_update_cacher(clear=True)
-                    self.obj.is_copy = None
+                    self.obj._is_copy = None
 
                     nindexer.append(labels.get_loc(key))
 


Test patch:
diff --git a/pandas/tests/indexes/test_multi.py b/pandas/tests/indexes/test_multi.py
--- a/pandas/tests/indexes/test_multi.py
+++ b/pandas/tests/indexes/test_multi.py
@@ -475,10 +475,10 @@ def test_set_value_keeps_names(self):
             columns=['one', 'two', 'three', 'four'],
             index=idx)
         df = df.sort_index()
-        assert df.is_copy is None
+        assert df._is_copy is None
         assert df.index.names == ('Name', 'Number')
         df.at[('grethe', '4'), 'one'] = 99.34
-        assert df.is_copy is None
+        assert df._is_copy is None
         assert df.index.names == ('Name', 'Number')
 
     def test_copy_names(self):
diff --git a/pandas/tests/indexing/test_chaining_and_caching.py b/pandas/tests/indexing/test_chaining_and_caching.py
--- a/pandas/tests/indexing/test_chaining_and_caching.py
+++ b/pandas/tests/indexing/test_chaining_and_caching.py
@@ -136,7 +136,7 @@ def test_detect_chained_assignment(self):
         expected = DataFrame([[-5, 1], [-6, 3]], columns=list('AB'))
         df = DataFrame(np.arange(4).reshape(2, 2),
                        columns=list('AB'), dtype='int64')
-        assert df.is_copy is None
+        assert df._is_copy is None
 
         df['A'][0] = -5
         df['A'][1] = -6
@@ -145,7 +145,7 @@ def test_detect_chained_assignment(self):
         # test with the chaining
         df = DataFrame({'A': Series(range(2), dtype='int64'),
                         'B': np.array(np.arange(2, 4), dtype=np.float64)})
-        assert df.is_copy is None
+        assert df._is_copy is None
 
         with pytest.raises(com.SettingWithCopyError):
             df['A'][0] = -5
@@ -153,7 +153,7 @@ def test_detect_chained_assignment(self):
         with pytest.raises(com.SettingWithCopyError):
             df['A'][1] = np.nan
 
-        assert df['A'].is_copy is None
+        assert df['A']._is_copy is None
 
         # Using a copy (the chain), fails
         df = DataFrame({'A': Series(range(2), dtype='int64'),
@@ -166,7 +166,7 @@ def test_detect_chained_assignment(self):
         df = DataFrame({'a': ['one', 'one', 'two', 'three',
                               'two', 'one', 'six'],
                         'c': Series(range(7), dtype='int64')})
-        assert df.is_copy is None
+        assert df._is_copy is None
 
         with pytest.raises(com.SettingWithCopyError):
             indexer = df.a.str.startswith('o')
@@ -186,7 +186,7 @@ def test_detect_chained_assignment(self):
 
         # gh-5475: Make sure that is_copy is picked up reconstruction
         df = DataFrame({"A": [1, 2]})
-        assert df.is_copy is None
+        assert df._is_copy is None
 
         with tm.ensure_clean('__tmp__pickle') as path:
             df.to_pickle(path)
@@ -211,16 +211,16 @@ def random_text(nobs=100):
 
         # Always a copy
         x = df.iloc[[0, 1, 2]]
-        assert x.is_copy is not None
+        assert x._is_copy is not None
 
         x = df.iloc[[0, 1, 2, 4]]
-        assert x.is_copy is not None
+        assert x._is_copy is not None
 
         # Explicitly copy
         indexer = df.letters.apply(lambda x: len(x) > 10)
         df = df.loc[indexer].copy()
 
-        assert df.is_copy is None
+        assert df._is_copy is None
         df['letters'] = df['letters'].apply(str.lower)
 
         # Implicitly take
@@ -228,7 +228,7 @@ def random_text(nobs=100):
         indexer = df.letters.apply(lambda x: len(x) > 10)
         df = df.loc[indexer]
 
-        assert df.is_copy is not None
+        assert df._is_copy is not None
         df['letters'] = df['letters'].apply(str.lower)
 
         # Implicitly take 2
@@ -236,14 +236,14 @@ def random_text(nobs=100):
         indexer = df.letters.apply(lambda x: len(x) > 10)
 
         df = df.loc[indexer]
-        assert df.is_copy is not None
+        assert df._is_copy is not None
         df.loc[:, 'letters'] = df['letters'].apply(str.lower)
 
         # Should be ok even though it's a copy!
-        assert df.is_copy is None
+        assert df._is_copy is None
 
         df['letters'] = df['letters'].apply(str.lower)
-        assert df.is_copy is None
+        assert df._is_copy is None
 
         df = random_text(100000)
         indexer = df.letters.apply(lambda x: len(x) > 10)
@@ -252,7 +252,7 @@ def random_text(nobs=100):
 
         # an identical take, so no copy
         df = DataFrame({'a': [1]}).dropna()
-        assert df.is_copy is None
+        assert df._is_copy is None
         df['a'] += 1
 
         # Inplace ops, originally from:
@@ -418,3 +418,14 @@ def test_cache_updating(self):
         tm.assert_frame_equal(df, expected)
         expected = Series([0, 0, 0, 2, 0], name='f')
         tm.assert_series_equal(df.f, expected)
+
+    def test_deprecate_is_copy(self):
+        # GH18801
+        df = DataFrame({"A": [1, 2, 3]})
+        with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
+            # getter
+            df.is_copy
+
+        with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
+            # setter
+            df.is_copy = "test deprecated is_copy"
diff --git a/pandas/tests/test_panel.py b/pandas/tests/test_panel.py
--- a/pandas/tests/test_panel.py
+++ b/pandas/tests/test_panel.py
@@ -607,7 +607,7 @@ def test_xs(self):
             # Mixed-type yields a copy.
             self.panel['strings'] = 'foo'
             result = self.panel.xs('D', axis=2)
-            assert result.is_copy is not None
+            assert result._is_copy is not None
 
     def test_getitem_fancy_labels(self):
         with catch_warnings(record=True):
diff --git a/pandas/tests/test_panel4d.py b/pandas/tests/test_panel4d.py
--- a/pandas/tests/test_panel4d.py
+++ b/pandas/tests/test_panel4d.py
@@ -510,7 +510,7 @@ def test_xs(self):
         with catch_warnings(record=True):
             result = self.panel4d.xs('D', axis=3)
 
-        assert result.is_copy is not None
+        assert result._is_copy is not None
 
     def test_getitem_fancy_labels(self):
         with catch_warnings(record=True):