Repository: pydata/xarray

Issue:
Re-indexing causes coordinates to be dropped
Hi, I encounter a problem with the index being dropped when I rename a dimension and stack it afterwards:

#### MCVE Code Sample

```python
ds = xr.Dataset({
    "test": xr.DataArray(
        [[[1,2],[3,4]], [[1,2],[3,4]]], 
        dims=("genes", "observations", "subtissues"), 
        coords={
            "observations": xr.DataArray(["x-1", "y-1"], dims=("observations",)),
            "individuals": xr.DataArray(["x", "y"], dims=("observations",)), 
            "genes": xr.DataArray(["a", "b"], dims=("genes",)), 
            "subtissues": xr.DataArray(["c", "d"], dims=("subtissues",)),
        }
    )
})

```
`individuals` is set here:
```python3
print(ds.rename_dims(observations="individuals"))
```
```
<xarray.Dataset>
Dimensions:       (genes: 2, individuals: 2, subtissues: 2)
Coordinates:
    observations  (individuals) <U3 'x-1' 'y-1'
  * individuals   (individuals) <U1 'x' 'y'
  * genes         (genes) <U1 'a' 'b'
  * subtissues    (subtissues) <U1 'c' 'd'
Data variables:
    test          (genes, individuals, subtissues) int64 1 2 3 4 1 2 3 4
```
Stacking caused `individuals` to disappear and be replaced with integers:
```python3
print(ds.rename_dims(observations="individuals").stack(observations=["individuals", "genes"]))
```
```
<xarray.Dataset>
Dimensions:       (observations: 4, subtissues: 2)
Coordinates:
  * observations  (observations) MultiIndex
  - individuals   (observations) int64 0 0 1 1
  - genes         (observations) object 'a' 'b' 'a' 'b'
  * subtissues    (subtissues) <U1 'c' 'd'
Data variables:
    test          (subtissues, observations) int64 1 1 3 3 2 2 4 4
```
Explicitly setting `individuals` keeps them correctly after stacking:
```python3
print(ds.rename_dims(observations="individuals").set_index({"individuals": "individuals"}).set_coords("individuals").stack(observations=["individuals", "genes"]))
```
```
<xarray.Dataset>
Dimensions:       (observations: 4, subtissues: 2)
Coordinates:
  * observations  (observations) MultiIndex
  - individuals   (observations) object 'x' 'x' 'y' 'y'
  - genes         (observations) object 'a' 'b' 'a' 'b'
  * subtissues    (subtissues) <U1 'c' 'd'
Data variables:
    test          (subtissues, observations) int64 1 1 3 3 2 2 4 4
```

Is this by intention?

#### Output of ``xr.show_versions()``
<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.7.4 (default, Aug 13 2019, 20:35:49) 
[GCC 7.3.0]
python-bits: 64
OS: Linux
OS-release: 3.10.0-957.10.1.el7.x86_64
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8
libhdf5: 1.10.4
libnetcdf: 4.6.1

xarray: 0.14.0
pandas: 0.25.1
numpy: 1.17.2
scipy: 1.3.1
netCDF4: 1.4.2
pydap: None
h5netcdf: 0.7.4
h5py: 2.9.0
Nio: None
zarr: 2.3.2
cftime: 1.0.3.4
nc_time_axis: None
PseudoNetCDF: None
rasterio: None
cfgrib: None
iris: None
bottleneck: None
dask: 2.5.2
distributed: 2.5.2
matplotlib: 3.1.1
cartopy: None
seaborn: 0.9.0
numbagg: None
setuptools: 41.4.0
pip: 19.2.3
conda: None
pytest: 5.0.1
IPython: 7.8.0
sphinx: None



</details>


Discussion:
keewis: I used `bug fixes` as the category in `whats-new.rst` but this could be something else as well

Patch:
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 70853dbb..5a9f2497 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -37,6 +37,9 @@ New Features
 - Added the ``count`` reduction method to both :py:class:`~core.rolling.DatasetCoarsen`
   and :py:class:`~core.rolling.DataArrayCoarsen` objects. (:pull:`3500`)
   By `Deepak Cherian <https://github.com/dcherian>`_
+- :py:meth:`Dataset.swap_dims` and :py:meth:`DataArray.swap_dims`
+  now allow swapping to dimension names that don't exist yet. (:pull:`3636`)
+  By `Justus Magin <https://github.com/keewis>`_.
 - Extend :py:class:`core.accessor_dt.DatetimeAccessor` properties 
   and support `.dt` accessor for timedelta 
   via :py:class:`core.accessor_dt.TimedeltaAccessor` (:pull:`3612`)
@@ -62,6 +65,9 @@ Bug fixes
   By `Tom Augspurger <https://github.com/TomAugspurger>`_.
 - Ensure :py:meth:`Dataset.quantile`, :py:meth:`DataArray.quantile` issue the correct error
   when ``q`` is out of bounds (:issue:`3634`) by `Mathias Hauser <https://github.com/mathause>`_.
+- Raise an error when trying to use :py:meth:`Dataset.rename_dims` to
+  rename to an existing name (:issue:`3438`, :pull:`3645`)
+  By `Justus Magin <https://github.com/keewis>`_.
 - :py:meth:`Dataset.rename`, :py:meth:`DataArray.rename` now check for conflicts with
   MultiIndex level names.
 
diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py
index 31aa4da5..cbd8d243 100644
--- a/xarray/core/dataarray.py
+++ b/xarray/core/dataarray.py
@@ -1480,8 +1480,7 @@ class DataArray(AbstractArray, DataWithCoords):
         ----------
         dims_dict : dict-like
             Dictionary whose keys are current dimension names and whose values
-            are new names. Each value must already be a coordinate on this
-            array.
+            are new names.
 
         Returns
         -------
@@ -1504,6 +1503,13 @@ class DataArray(AbstractArray, DataWithCoords):
         Coordinates:
             x        (y) <U1 'a' 'b'
           * y        (y) int64 0 1
+        >>> arr.swap_dims({"x": "z"})
+        <xarray.DataArray (z: 2)>
+        array([0, 1])
+        Coordinates:
+            x        (z) <U1 'a' 'b'
+            y        (z) int64 0 1
+        Dimensions without coordinates: z
 
         See Also
         --------
diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py
index 032c66d3..ac0a923d 100644
--- a/xarray/core/dataset.py
+++ b/xarray/core/dataset.py
@@ -2798,7 +2798,8 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords):
         ----------
         dims_dict : dict-like, optional
             Dictionary whose keys are current dimension names and
-            whose values are the desired names.
+            whose values are the desired names. The desired names must
+            not be the name of an existing dimension or Variable in the Dataset.
         **dims, optional
             Keyword form of ``dims_dict``.
             One of dims_dict or dims must be provided.
@@ -2816,12 +2817,17 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords):
         DataArray.rename
         """
         dims_dict = either_dict_or_kwargs(dims_dict, dims, "rename_dims")
-        for k in dims_dict:
+        for k, v in dims_dict.items():
             if k not in self.dims:
                 raise ValueError(
                     "cannot rename %r because it is not a "
                     "dimension in this dataset" % k
                 )
+            if v in self.dims or v in self:
+                raise ValueError(
+                    f"Cannot rename {k} to {v} because {v} already exists. "
+                    "Try using swap_dims instead."
+                )
 
         variables, coord_names, sizes, indexes = self._rename_all(
             name_dict={}, dims_dict=dims_dict
@@ -2875,8 +2881,7 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords):
         ----------
         dims_dict : dict-like
             Dictionary whose keys are current dimension names and whose values
-            are new names. Each value must already be a variable in the
-            dataset.
+            are new names.
 
         Returns
         -------
@@ -2905,6 +2910,16 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords):
         Data variables:
             a        (y) int64 5 7
             b        (y) float64 0.1 2.4
+        >>> ds.swap_dims({"x": "z"})
+        <xarray.Dataset>
+        Dimensions:  (z: 2)
+        Coordinates:
+            x        (z) <U1 'a' 'b'
+            y        (z) int64 0 1
+        Dimensions without coordinates: z
+        Data variables:
+            a        (z) int64 5 7
+            b        (z) float64 0.1 2.4
 
         See Also
         --------
@@ -2921,7 +2936,7 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords):
                     "cannot swap from dimension %r because it is "
                     "not an existing dimension" % k
                 )
-            if self.variables[v].dims != (k,):
+            if v in self.variables and self.variables[v].dims != (k,):
                 raise ValueError(
                     "replacement dimension %r is not a 1D "
                     "variable along the old dimension %r" % (v, k)
@@ -2930,7 +2945,7 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords):
         result_dims = {dims_dict.get(dim, dim) for dim in self.dims}
 
         coord_names = self._coord_names.copy()
-        coord_names.update(dims_dict.values())
+        coord_names.update({dim for dim in dims_dict.values() if dim in self.variables})
 
         variables: Dict[Hashable, Variable] = {}
         indexes: Dict[Hashable, pd.Index] = {}


Test patch:
diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py
index f957316d..4189c3b5 100644
--- a/xarray/tests/test_dataarray.py
+++ b/xarray/tests/test_dataarray.py
@@ -1530,6 +1530,11 @@ class TestDataArray:
         actual = array.swap_dims({"x": "y"})
         assert_identical(expected, actual)
 
+        array = DataArray(np.random.randn(3), {"x": list("abc")}, "x")
+        expected = DataArray(array.values, {"x": ("y", list("abc"))}, dims="y")
+        actual = array.swap_dims({"x": "y"})
+        assert_identical(expected, actual)
+
     def test_expand_dims_error(self):
         array = DataArray(
             np.random.randn(3, 4),
diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py
index edda4cb2..2220abbe 100644
--- a/xarray/tests/test_dataset.py
+++ b/xarray/tests/test_dataset.py
@@ -2444,6 +2444,9 @@ class TestDataset:
         with pytest.raises(ValueError):
             original.rename_dims(dims_dict_bad)
 
+        with pytest.raises(ValueError):
+            original.rename_dims({"x": "z"})
+
     def test_rename_vars(self):
         original = Dataset({"x": ("x", [0, 1, 2]), "y": ("x", [10, 11, 12]), "z": 42})
         expected = Dataset(
@@ -2533,6 +2536,12 @@ class TestDataset:
         with raises_regex(ValueError, "replacement dimension"):
             original.swap_dims({"x": "z"})
 
+        expected = Dataset(
+            {"y": ("u", list("abc")), "z": 42}, coords={"x": ("u", [1, 2, 3])}
+        )
+        actual = original.swap_dims({"x": "u"})
+        assert_identical(expected, actual)
+
     def test_expand_dims_error(self):
         original = Dataset(
             {