Repository: pydata/xarray

Issue:
Get ready for pandas 3 copy-on-write
### What is your issue?

This line fails with `pd.set_options("mode.copy_on_write", True)`
https://github.com/pydata/xarray/blob/c9d3084e98d38a7a9488380789a8d0acfde3256f/xarray/tests/__init__.py#L329

We'll need to fix this before Pandas 3 is released in April: https://github.com/pydata/xarray/blob/c9d3084e98d38a7a9488380789a8d0acfde3256f/xarray/tests/__init__.py#L329

Here's a test
```python
def example():
    obj = Dataset()
    obj["dim2"] = ("dim2", 0.5 * np.arange(9))
    obj["time"] = ("time", pd.date_range("2000-01-01", periods=20)
    print({k: v.data.flags for k, v in obj.variables.items()})
    return obj

example()
pd.set_options("mode.copy_on_write", True)
example()
```


Discussion:
I would recommend to either copy the data they come from a pandas object or set the flags to writeable if you are ok with the implications or if you don't modify the data inplace anyway. That said, modifying the underlying data of an Index is a __really__ bad idea if you or your users want to re-use the Index. That can lead to segfaults among other things

Be aware if you set the flag to writeable, this is not possible in all scenarios, e.g.

```
idx = pd.Index([1, 2, 3], dtype="int64[pyarrow]")
vals = idx.to_numpy()
vals.flags.writeable = True
```

This will raise, arrow protects against this and does not allow you to set the flag to writeable
Thanks @phofl  I suspect this assertion is overly broad and we didn't want to do it for Index-backed Xarray variables.



Patch:
diff --git a/xarray/core/variable.py b/xarray/core/variable.py
index a03e93ac699..cad48d0775a 100644
--- a/xarray/core/variable.py
+++ b/xarray/core/variable.py
@@ -209,7 +209,14 @@ def _possibly_convert_objects(values):
     as_series = pd.Series(values.ravel(), copy=False)
     if as_series.dtype.kind in "mM":
         as_series = _as_nanosecond_precision(as_series)
-    return np.asarray(as_series).reshape(values.shape)
+    result = np.asarray(as_series).reshape(values.shape)
+    if not result.flags.writeable:
+        # GH8843, pandas copy-on-write mode creates read-only arrays by default
+        try:
+            result.flags.writeable = True
+        except ValueError:
+            result = result.copy()
+    return result
 
 
 def _possibly_convert_datetime_or_timedelta_index(data):


Test patch:
diff --git a/xarray/tests/__init__.py b/xarray/tests/__init__.py
index 2e6e638f5b1..5007db9eeb2 100644
--- a/xarray/tests/__init__.py
+++ b/xarray/tests/__init__.py
@@ -20,6 +20,7 @@
 from xarray.core.duck_array_ops import allclose_or_equiv  # noqa: F401
 from xarray.core.indexing import ExplicitlyIndexed
 from xarray.core.options import set_options
+from xarray.core.variable import IndexVariable
 from xarray.testing import (  # noqa: F401
     assert_chunks_equal,
     assert_duckarray_allclose,
@@ -47,6 +48,15 @@
 )
 
 
+def assert_writeable(ds):
+    readonly = [
+        name
+        for name, var in ds.variables.items()
+        if not isinstance(var, IndexVariable) and not var.data.flags.writeable
+    ]
+    assert not readonly, readonly
+
+
 def _importorskip(
     modname: str, minversion: str | None = None
 ) -> tuple[bool, pytest.MarkDecorator]:
@@ -326,7 +336,7 @@ def create_test_data(
         numbers_values = np.random.randint(0, 3, _dims["dim3"], dtype="int64")
     obj.coords["numbers"] = ("dim3", numbers_values)
     obj.encoding = {"foo": "bar"}
-    assert all(obj.data.flags.writeable for obj in obj.variables.values())
+    assert_writeable(obj)
     return obj
 
 
diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py
index b97d5ced938..3fb137977e8 100644
--- a/xarray/tests/test_backends.py
+++ b/xarray/tests/test_backends.py
@@ -2605,7 +2605,9 @@ def test_append_with_append_dim_no_overwrite(self) -> None:
             # overwrite a coordinate;
             # for mode='a-', this will not get written to the store
             # because it does not have the append_dim as a dim
-            ds_to_append.lon.data[:] = -999
+            lon = ds_to_append.lon.to_numpy().copy()
+            lon[:] = -999
+            ds_to_append["lon"] = lon
             ds_to_append.to_zarr(
                 store_target, mode="a-", append_dim="time", **self.version_kwargs
             )
@@ -2615,7 +2617,9 @@ def test_append_with_append_dim_no_overwrite(self) -> None:
             # by default, mode="a" will overwrite all coordinates.
             ds_to_append.to_zarr(store_target, append_dim="time", **self.version_kwargs)
             actual = xr.open_dataset(store_target, engine="zarr", **self.version_kwargs)
-            original2.lon.data[:] = -999
+            lon = original2.lon.to_numpy().copy()
+            lon[:] = -999
+            original2["lon"] = lon
             assert_identical(original2, actual)
 
     @requires_dask
diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py
index 4937fc5f3a3..d2b8634b8b9 100644
--- a/xarray/tests/test_dataset.py
+++ b/xarray/tests/test_dataset.py
@@ -51,6 +51,7 @@
     assert_equal,
     assert_identical,
     assert_no_warnings,
+    assert_writeable,
     create_test_data,
     has_cftime,
     has_dask,
@@ -96,11 +97,11 @@ def create_append_test_data(seed=None) -> tuple[Dataset, Dataset, Dataset]:
     nt2 = 2
     time1 = pd.date_range("2000-01-01", periods=nt1)
     time2 = pd.date_range("2000-02-01", periods=nt2)
-    string_var = np.array(["ae", "bc", "df"], dtype=object)
+    string_var = np.array(["a", "bc", "def"], dtype=object)
     string_var_to_append = np.array(["asdf", "asdfg"], dtype=object)
     string_var_fixed_length = np.array(["aa", "bb", "cc"], dtype="|S2")
     string_var_fixed_length_to_append = np.array(["dd", "ee"], dtype="|S2")
-    unicode_var = ["áó", "áó", "áó"]
+    unicode_var = np.array(["áó", "áó", "áó"])
     datetime_var = np.array(
         ["2019-01-01", "2019-01-02", "2019-01-03"], dtype="datetime64[s]"
     )
@@ -119,17 +120,11 @@ def create_append_test_data(seed=None) -> tuple[Dataset, Dataset, Dataset]:
                     coords=[lat, lon, time1],
                     dims=["lat", "lon", "time"],
                 ),
-                "string_var": xr.DataArray(string_var, coords=[time1], dims=["time"]),
-                "string_var_fixed_length": xr.DataArray(
-                    string_var_fixed_length, coords=[time1], dims=["time"]
-                ),
-                "unicode_var": xr.DataArray(
-                    unicode_var, coords=[time1], dims=["time"]
-                ).astype(np.str_),
-                "datetime_var": xr.DataArray(
-                    datetime_var, coords=[time1], dims=["time"]
-                ),
-                "bool_var": xr.DataArray(bool_var, coords=[time1], dims=["time"]),
+                "string_var": ("time", string_var),
+                "string_var_fixed_length": ("time", string_var_fixed_length),
+                "unicode_var": ("time", unicode_var),
+                "datetime_var": ("time", datetime_var),
+                "bool_var": ("time", bool_var),
             }
         )
 
@@ -140,21 +135,11 @@ def create_append_test_data(seed=None) -> tuple[Dataset, Dataset, Dataset]:
                     coords=[lat, lon, time2],
                     dims=["lat", "lon", "time"],
                 ),
-                "string_var": xr.DataArray(
-                    string_var_to_append, coords=[time2], dims=["time"]
-                ),
-                "string_var_fixed_length": xr.DataArray(
-                    string_var_fixed_length_to_append, coords=[time2], dims=["time"]
-                ),
-                "unicode_var": xr.DataArray(
-                    unicode_var[:nt2], coords=[time2], dims=["time"]
-                ).astype(np.str_),
-                "datetime_var": xr.DataArray(
-                    datetime_var_to_append, coords=[time2], dims=["time"]
-                ),
-                "bool_var": xr.DataArray(
-                    bool_var_to_append, coords=[time2], dims=["time"]
-                ),
+                "string_var": ("time", string_var_to_append),
+                "string_var_fixed_length": ("time", string_var_fixed_length_to_append),
+                "unicode_var": ("time", unicode_var[:nt2]),
+                "datetime_var": ("time", datetime_var_to_append),
+                "bool_var": ("time", bool_var_to_append),
             }
         )
 
@@ -168,8 +153,9 @@ def create_append_test_data(seed=None) -> tuple[Dataset, Dataset, Dataset]:
             }
         )
 
-    assert all(objp.data.flags.writeable for objp in ds.variables.values())
-    assert all(objp.data.flags.writeable for objp in ds_to_append.variables.values())
+    assert_writeable(ds)
+    assert_writeable(ds_to_append)
+    assert_writeable(ds_with_new_var)
     return ds, ds_to_append, ds_with_new_var
 
 
@@ -182,10 +168,8 @@ def make_datasets(data, data_to_append) -> tuple[Dataset, Dataset]:
         ds_to_append = xr.Dataset(
             {"temperature": (["time"], data_to_append)}, coords={"time": [0, 1, 2]}
         )
-        assert all(objp.data.flags.writeable for objp in ds.variables.values())
-        assert all(
-            objp.data.flags.writeable for objp in ds_to_append.variables.values()
-        )
+        assert_writeable(ds)
+        assert_writeable(ds_to_append)
         return ds, ds_to_append
 
     u2_strings = ["ab", "cd", "ef"]
@@ -2964,10 +2948,11 @@ def test_copy_coords(self, deep, expected_orig) -> None:
             name="value",
         ).to_dataset()
         ds_cp = ds.copy(deep=deep)
-        ds_cp.coords["a"].data[0] = 999
+        new_a = np.array([999, 2])
+        ds_cp.coords["a"] = ds_cp.a.copy(data=new_a)
 
         expected_cp = xr.DataArray(
-            xr.IndexVariable("a", np.array([999, 2])),
+            xr.IndexVariable("a", new_a),
             coords={"a": [999, 2]},
             dims=["a"],
         )
diff --git a/xarray/tests/test_missing.py b/xarray/tests/test_missing.py
index f13406d0acc..c1d1058fd6e 100644
--- a/xarray/tests/test_missing.py
+++ b/xarray/tests/test_missing.py
@@ -122,10 +122,13 @@ def test_interpolate_pd_compat(method, fill_value) -> None:
                 # for the numpy linear methods.
                 # see https://github.com/pandas-dev/pandas/issues/55144
                 # This aligns the pandas output with the xarray output
-                expected.values[pd.isnull(actual.values)] = np.nan
-                expected.values[actual.values == fill_value] = fill_value
+                fixed = expected.values.copy()
+                fixed[pd.isnull(actual.values)] = np.nan
+                fixed[actual.values == fill_value] = fill_value
+            else:
+                fixed = expected.values
 
-            np.testing.assert_allclose(actual.values, expected.values)
+            np.testing.assert_allclose(actual.values, fixed)
 
 
 @requires_scipy
diff --git a/xarray/tests/test_variable.py b/xarray/tests/test_variable.py
index 73f5abe66e5..061510f2515 100644
--- a/xarray/tests/test_variable.py
+++ b/xarray/tests/test_variable.py
@@ -64,6 +64,21 @@ def var():
     return Variable(dims=list("xyz"), data=np.random.rand(3, 4, 5))
 
 
+@pytest.mark.parametrize(
+    "data",
+    [
+        np.array(["a", "bc", "def"], dtype=object),
+        np.array(["2019-01-01", "2019-01-02", "2019-01-03"], dtype="datetime64[ns]"),
+    ],
+)
+def test_as_compatible_data_writeable(data):
+    pd.set_option("mode.copy_on_write", True)
+    # GH8843, ensure writeable arrays for data_vars even with
+    # pandas copy-on-write mode
+    assert as_compatible_data(data).flags.writeable
+    pd.reset_option("mode.copy_on_write")
+
+
 class VariableSubclassobjects(NamedArraySubclassobjects, ABC):
     @pytest.fixture
     def target(self, data):