Repository: pydata/xarray

Issue:
Grouper object does not handle IndexVariable
### What happened?

Since #7561 with xarray-2023.5.0, the new grouper object raises an unexpected exception with an IndexVariable.

### What did you expect to happen?

With xarray-2023.3.0 there was no issue, the grouper operation returned a new DataArray object.

### Minimal Complete Verifiable Example

```Python
import numpy as np
import pandas as pd
import xarray as xr

da = xr.DataArray(
    np.linspace(0, 1826, num=1827),
    coords=[pd.date_range("2000-01-01", "2004-12-31", freq="D")],
    dims="time",
)
iv = xr.IndexVariable(dims=("time",), data=pd.Index(da.time.dt.year))

# This is where the exception is raised
m = da.groupby(iv).mean()
print(m)
```


### MVCE confirmation

- [X] Minimal example — the example is as focused as reasonably possible to demonstrate the underlying issue in xarray.
- [X] Complete example — the example is self-contained, including all data and the text of any traceback.
- [X] Verifiable example — the example copy & pastes into an IPython prompt or [Binder notebook](https://mybinder.org/v2/gh/pydata/xarray/main?urlpath=lab/tree/doc/examples/blank_template.ipynb), returning the result.
- [X] New issue — a search of GitHub Issues suggests this is not a duplicate.

### Relevant log output

```Python
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
Cell In[1], line 13
     10 iv = xr.IndexVariable(dims=("time",), data=pd.Index(da.time.dt.year))
     12 # This is where the exception is raised
---> 13 m = da.groupby(iv).mean()
     14 print(m)

File /tmp/py310/lib/python3.10/site-packages/xarray/core/dataarray.py:6503, in DataArray.groupby(self, group, squeeze, restore_coord_dims)
   6495 from xarray.core.groupby import (
   6496     DataArrayGroupBy,
   6497     ResolvedUniqueGrouper,
   6498     UniqueGrouper,
   6499     _validate_groupby_squeeze,
   6500 )
   6502 _validate_groupby_squeeze(squeeze)
-> 6503 rgrouper = ResolvedUniqueGrouper(UniqueGrouper(), group, self)
   6504 return DataArrayGroupBy(
   6505     self,
   6506     (rgrouper,),
   6507     squeeze=squeeze,
   6508     restore_coord_dims=restore_coord_dims,
   6509 )

File <string>:6, in __init__(self, grouper, group, obj)

File /tmp/py310/lib/python3.10/site-packages/xarray/core/groupby.py:335, in ResolvedGrouper.__post_init__(self)
    334 def __post_init__(self) -> None:
--> 335     self.group: T_Group = _resolve_group(self.obj, self.group)
    337     (
    338         self.group1d,
    339         self.stacked_obj,
    340         self.stacked_dim,
    341         self.inserted_dims,
    342     ) = _ensure_1d(group=self.group, obj=self.obj)

File /tmp/py310/lib/python3.10/site-packages/xarray/core/groupby.py:640, in _resolve_group(obj, group)
    637     else:
    638         newgroup = group
--> 640 if newgroup.size == 0:
    641     raise ValueError(f"{newgroup.name} must not be empty")
    643 return newgroup

UnboundLocalError: local variable 'newgroup' referenced before assignment
```


### Anything else we need to know?

With xarray-2023.3.0 the output for the example is:
```
<xarray.DataArray (time: 5)>
array([ 182.5,  548. ,  913. , 1278. , 1643.5])
Coordinates:
  * time     (time) int64 2000 2001 2002 2003 2004
```

### Environment

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0]
python-bits: 64
OS: Linux
OS-release: 4.4.0-19041-Microsoft
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: C.UTF-8
LOCALE: ('en_US', 'UTF-8')
libhdf5: None
libnetcdf: None

xarray: 2023.5.0
pandas: 1.5.3
numpy: 1.24.3
scipy: None
netCDF4: None
pydap: None
h5netcdf: None
h5py: None
Nio: None
zarr: None
cftime: None
nc_time_axis: None
PseudoNetCDF: None
iris: None
bottleneck: None
dask: None
distributed: None
matplotlib: 3.7.1
cartopy: None
seaborn: None
numbagg: None
fsspec: None
cupy: None
pint: None
sparse: None
flox: None
numpy_groupies: None
setuptools: 59.6.0
pip: 23.1.2
conda: None
pytest: 7.3.1
mypy: None
IPython: 8.13.2
sphinx: None

</details>

Discussion:
mwtoews: The fix works without flox. Should it work _with_ flox?
dcherian: Yes we should support it by casting to a DataArray in `_flox_reduce`
mwtoews: @dcherian what about casting `DataArray(group)` earlier? This seems to work with and without flox.
dcherian: That's fine too, just unnecessary for the normal code path. :)

Patch:
diff --git a/.github/workflows/pypi-release.yaml b/.github/workflows/pypi-release.yaml
index cdfdba4e..b2935928 100644
--- a/.github/workflows/pypi-release.yaml
+++ b/.github/workflows/pypi-release.yaml
@@ -70,12 +70,26 @@ jobs:
           python -m pip install dist/xarray*.whl
           python -m xarray.util.print_versions
 
+  upload-to-test-pypi:
+    needs: test-built-dist
+    if: github.event_name == 'push'
+    runs-on: ubuntu-latest
+
+    environment:
+      name: pypi
+      url: https://test.pypi.org/p/xarray
+    permissions:
+      id-token: write
+
+    steps:
+      - uses: actions/download-artifact@v3
+        with:
+          name: releases
+          path: dist
       - name: Publish package to TestPyPI
         if: github.event_name == 'push'
         uses: pypa/gh-action-pypi-publish@v1.8.6
         with:
-          user: __token__
-          password: ${{ secrets.TESTPYPI_TOKEN }}
           repository_url: https://test.pypi.org/legacy/
           verbose: true
 
@@ -84,6 +98,13 @@ jobs:
     needs: test-built-dist
     if: github.event_name == 'release'
     runs-on: ubuntu-latest
+
+    environment:
+      name: pypi
+      url: https://pypi.org/p/xarray
+    permissions:
+      id-token: write
+
     steps:
       - uses: actions/download-artifact@v3
         with:
@@ -92,6 +113,4 @@ jobs:
       - name: Publish package to PyPI
         uses: pypa/gh-action-pypi-publish@v1.8.6
         with:
-          user: __token__
-          password: ${{ secrets.PYPI_TOKEN }}
           verbose: true
diff --git a/HOW_TO_RELEASE.md b/HOW_TO_RELEASE.md
index 24a2dba6..34a63aad 100644
--- a/HOW_TO_RELEASE.md
+++ b/HOW_TO_RELEASE.md
@@ -59,7 +59,7 @@ upstream        https://github.com/pydata/xarray (push)
  10. This should automatically trigger an upload of the new build to PyPI via GitHub Actions.
     Check this has run [here](https://github.com/pydata/xarray/actions/workflows/pypi-release.yaml),
     and that the version number you expect is displayed [on PyPI](https://pypi.org/project/xarray/)
-11. Add a section for the next release {YYYY.MM.X+1} to doc/whats-new.rst:
+11. Add a section for the next release {YYYY.MM.X+1} to doc/whats-new.rst (we avoid doing this earlier so that it doesn't show up in the RTD build):
      ```rst
      .. _whats-new.YYYY.MM.X+1:
 
diff --git a/ci/requirements/doc.yml b/ci/requirements/doc.yml
index 115d7dfa..18c79ee9 100644
--- a/ci/requirements/doc.yml
+++ b/ci/requirements/doc.yml
@@ -7,9 +7,11 @@ dependencies:
   - python=3.10
   - bottleneck
   - cartopy
+  - cfgrib
   - dask-core>=2022.1
   - h5netcdf>=0.13
   - ipykernel
+  - ipywidgets  # silence nbsphinx warning
   - ipython
   - iris>=2.3
   - jupyter_client
@@ -24,7 +26,6 @@ dependencies:
   - pip
   - pre-commit
   - pyproj
-  - rasterio>=1.1
   - scipy!=1.10.0
   - seaborn
   - setuptools
diff --git a/doc/conf.py b/doc/conf.py
index e8ef13f1..f201af85 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -58,7 +58,7 @@ except ImportError:
         ]
     )
 
-nbsphinx_allow_errors = True
+nbsphinx_allow_errors = False
 
 # -- General configuration ------------------------------------------------
 
diff --git a/doc/developers-meeting.rst b/doc/developers-meeting.rst
index 1c49a900..153f3520 100644
--- a/doc/developers-meeting.rst
+++ b/doc/developers-meeting.rst
@@ -3,18 +3,18 @@ Developers meeting
 
 Xarray developers meet bi-weekly every other Wednesday.
 
-The meeting occurs on `Zoom <https://us02web.zoom.us/j/88251613296?pwd=azZsSkU1UWJZTVFKNnhIUVdZcENUZz09>`__.
+The meeting occurs on `Zoom <https://us02web.zoom.us/j/87503265754?pwd=cEFJMzFqdTFaS3BMdkx4UkNZRk1QZz09>`__.
 
-Find the `notes for the meeting here <https://hackmd.io/@U4W-olO3TX-hc-cvbjNe4A/xarray-dev-meeting/edit>`__.
+Find the `notes for the meeting here <https://hackmd.io/fx7KNO2vTKutZeUysE-SIA/edit>`__.
 
 There is a :issue:`GitHub issue for changes to the meeting<4001>`.
 
 You can subscribe to this calendar to be notified of changes:
 
-* `Google Calendar <https://calendar.google.com/calendar/embed?src=ucar.edu_2gjd5fuugcj4ol6ij7knj8krn8%40group.calendar.google.com&ctz=America%2FLos_Angeles>`__
-* `iCal <https://calendar.google.com/calendar/ical/ucar.edu_2gjd5fuugcj4ol6ij7knj8krn8%40group.calendar.google.com/public/basic.ics>`__
+* `Google Calendar <https://calendar.google.com/calendar/embed?src=59589f9634ab4ef304e8209be66cda9812dababca71eb8a01a6fa2d167f90d94%40group.calendar.google.com&ctz=America%2FLos_Angeles>`__
+* `iCal <https://calendar.google.com/calendar/ical/59589f9634ab4ef304e8209be66cda9812dababca71eb8a01a6fa2d167f90d94%40group.calendar.google.com/public/basic.ics>`__
 
 .. raw:: html
 
-   <iframe id="calendariframe" src="https://calendar.google.com/calendar/embed?ctz=local&amp;src=ucar.edu_2gjd5fuugcj4ol6ij7knj8krn8%40group.calendar.google.com" style="border: 0" width="800" height="600" frameborder="0" scrolling="no"></iframe>
+   <iframe src="https://calendar.google.com/calendar/embed?src=59589f9634ab4ef304e8209be66cda9812dababca71eb8a01a6fa2d167f90d94%40group.calendar.google.com&ctz=America%2FLos_Angeles" style="border: 0" width="800" height="600" frameborder="0" scrolling="no"></iframe>
    <script>document.getElementById("calendariframe").src = document.getElementById("calendariframe").src.replace("ctz=local", "ctz=" + Intl.DateTimeFormat().resolvedOptions().timeZone)</script>
diff --git a/doc/examples/visualization_gallery.ipynb b/doc/examples/visualization_gallery.ipynb
index e6fa564d..e7e9196a 100644
--- a/doc/examples/visualization_gallery.ipynb
+++ b/doc/examples/visualization_gallery.ipynb
@@ -193,90 +193,6 @@
     "# Show\n",
     "plt.tight_layout()"
    ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {
-    "jp-MarkdownHeadingCollapsed": true,
-    "tags": []
-   },
-   "source": [
-    "## `imshow()` and rasterio map projections\n",
-    "\n",
-    "\n",
-    "Using rasterio's projection information for more accurate plots.\n",
-    "\n",
-    "This example extends `recipes.rasterio` and plots the image in the\n",
-    "original map projection instead of relying on pcolormesh and a map\n",
-    "transformation."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "da = xr.tutorial.open_rasterio(\"RGB.byte\")\n",
-    "\n",
-    "# The data is in UTM projection. We have to set it manually until\n",
-    "# https://github.com/SciTools/cartopy/issues/813 is implemented\n",
-    "crs = ccrs.UTM(\"18\")\n",
-    "\n",
-    "# Plot on a map\n",
-    "ax = plt.subplot(projection=crs)\n",
-    "da.plot.imshow(ax=ax, rgb=\"band\", transform=crs)\n",
-    "ax.coastlines(\"10m\", color=\"r\")"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Parsing rasterio geocoordinates\n",
-    "\n",
-    "Converting a projection's cartesian coordinates into 2D longitudes and\n",
-    "latitudes.\n",
-    "\n",
-    "These new coordinates might be handy for plotting and indexing, but it should\n",
-    "be kept in mind that a grid which is regular in projection coordinates will\n",
-    "likely be irregular in lon/lat. It is often recommended to work in the data's\n",
-    "original map projection (see `recipes.rasterio_rgb`)."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from pyproj import Transformer\n",
-    "import numpy as np\n",
-    "\n",
-    "da = xr.tutorial.open_rasterio(\"RGB.byte\")\n",
-    "\n",
-    "x, y = np.meshgrid(da[\"x\"], da[\"y\"])\n",
-    "transformer = Transformer.from_crs(da.crs, \"EPSG:4326\", always_xy=True)\n",
-    "lon, lat = transformer.transform(x, y)\n",
-    "da.coords[\"lon\"] = ((\"y\", \"x\"), lon)\n",
-    "da.coords[\"lat\"] = ((\"y\", \"x\"), lat)\n",
-    "\n",
-    "# Compute a greyscale out of the rgb image\n",
-    "greyscale = da.mean(dim=\"band\")\n",
-    "\n",
-    "# Plot on a map\n",
-    "ax = plt.subplot(projection=ccrs.PlateCarree())\n",
-    "greyscale.plot(\n",
-    "    ax=ax,\n",
-    "    x=\"lon\",\n",
-    "    y=\"lat\",\n",
-    "    transform=ccrs.PlateCarree(),\n",
-    "    cmap=\"Greys_r\",\n",
-    "    shading=\"auto\",\n",
-    "    add_colorbar=False,\n",
-    ")\n",
-    "ax.coastlines(\"10m\", color=\"r\")"
-   ]
   }
  ],
  "metadata": {
@@ -296,6 +212,13 @@
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
    "version": "3.9.7"
+  },
+  "widgets": {
+   "application/vnd.jupyter.widget-state+json": {
+    "state": {},
+    "version_major": 2,
+    "version_minor": 0
+   }
   }
  },
  "nbformat": 4,
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 5c0d3c3c..59f91091 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -25,6 +25,10 @@ New Features
 
 - Added support for multidimensional initial guess and bounds in :py:meth:`DataArray.curvefit` (:issue:`7768`, :pull:`7821`).
   By `András Gunyhó <https://github.com/mgunyho>`_.
+- Add an ``errors`` option to :py:meth:`Dataset.curve_fit` that allows
+  returning NaN for the parameters and covariances of failed fits, rather than
+  failing the whole series of fits (:issue:`6317`, :pull:`7891`).
+  By `Dominik Stańczak <https://github.com/StanczakDominik>`_ and `András Gunyhó <https://github.com/mgunyho>`_.
 
 Breaking changes
 ~~~~~~~~~~~~~~~~
@@ -46,6 +50,10 @@ Bug fixes
   By `Mattia Almansi <https://github.com/malmans2>`_.
 - Don't call ``CachingFileManager.__del__`` on interpreter shutdown (:issue:`7814`, :pull:`7880`).
   By `Justus Magin <https://github.com/keewis>`_.
+- Preserve vlen dtype for empty string arrays (:issue:`7328`, :pull:`7862`).
+  By `Tom White <https://github.com/tomwhite>`_ and `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
+- Ensure dtype of reindex result matches dtype of the original DataArray (:issue:`7299`, :pull:`7917`)
+  By `Anderson Banihirwe <https://github.com/andersy005>`_.
 
 Documentation
 ~~~~~~~~~~~~~
diff --git a/xarray/backends/netCDF4_.py b/xarray/backends/netCDF4_.py
index d3866e90..8a5d48c8 100644
--- a/xarray/backends/netCDF4_.py
+++ b/xarray/backends/netCDF4_.py
@@ -65,10 +65,12 @@ class BaseNetCDF4Array(BackendArray):
 
         dtype = array.dtype
         if dtype is str:
-            # use object dtype because that's the only way in numpy to
-            # represent variable length strings; it also prevents automatic
-            # string concatenation via conventions.decode_cf_variable
-            dtype = np.dtype("O")
+            # use object dtype (with additional vlen string metadata) because that's
+            # the only way in numpy to represent variable length strings and to
+            # check vlen string dtype in further steps
+            # it also prevents automatic string concatenation via
+            # conventions.decode_cf_variable
+            dtype = coding.strings.create_vlen_dtype(str)
         self.dtype = dtype
 
     def __setitem__(self, key, value):
diff --git a/xarray/backends/zarr.py b/xarray/backends/zarr.py
index a4012a8a..5c3d5781 100644
--- a/xarray/backends/zarr.py
+++ b/xarray/backends/zarr.py
@@ -70,7 +70,14 @@ class ZarrArrayWrapper(BackendArray):
         array = self.get_array()
         self.shape = array.shape
 
-        dtype = array.dtype
+        # preserve vlen string object dtype (GH 7328)
+        if array.filters is not None and any(
+            [filt.codec_id == "vlen-utf8" for filt in array.filters]
+        ):
+            dtype = coding.strings.create_vlen_dtype(str)
+        else:
+            dtype = array.dtype
+
         self.dtype = dtype
 
     def get_array(self):
diff --git a/xarray/coding/strings.py b/xarray/coding/strings.py
index ffe1b1a8..d0bfb1a7 100644
--- a/xarray/coding/strings.py
+++ b/xarray/coding/strings.py
@@ -29,7 +29,8 @@ def check_vlen_dtype(dtype):
     if dtype.kind != "O" or dtype.metadata is None:
         return None
     else:
-        return dtype.metadata.get("element_type")
+        # check xarray (element_type) as well as h5py (vlen)
+        return dtype.metadata.get("element_type", dtype.metadata.get("vlen"))
 
 
 def is_unicode_dtype(dtype):
diff --git a/xarray/conventions.py b/xarray/conventions.py
index 1506efc3..053863ac 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -108,6 +108,10 @@ def ensure_dtype_not_object(var: Variable, name: T_Name = None) -> Variable:
     if var.dtype.kind == "O":
         dims, data, attrs, encoding = _var_as_tuple(var)
 
+        # leave vlen dtypes unchanged
+        if strings.check_vlen_dtype(data.dtype) is not None:
+            return var
+
         if is_duck_dask_array(data):
             warnings.warn(
                 "variable {} has data in the form of a dask array with "
diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py
index bb6a9b13..9635d678 100644
--- a/xarray/core/dataarray.py
+++ b/xarray/core/dataarray.py
@@ -6162,6 +6162,7 @@ class DataArray(
         p0: dict[str, float | DataArray] | None = None,
         bounds: dict[str, tuple[float | DataArray, float | DataArray]] | None = None,
         param_names: Sequence[str] | None = None,
+        errors: ErrorOptions = "raise",
         kwargs: dict[str, Any] | None = None,
     ) -> Dataset:
         """
@@ -6206,6 +6207,10 @@ class DataArray(
             this will be automatically determined by arguments of `func`. `param_names`
             should be manually supplied when fitting a function that takes a variable
             number of parameters.
+        errors : {"raise", "ignore"}, default: "raise"
+            If 'raise', any errors from the `scipy.optimize_curve_fit` optimization will
+            raise an exception. If 'ignore', the coefficients and covariances for the
+            coordinates where the fitting failed will be NaN.
         **kwargs : optional
             Additional keyword arguments to passed to scipy curve_fit.
 
@@ -6312,6 +6317,7 @@ class DataArray(
             p0=p0,
             bounds=bounds,
             param_names=param_names,
+            errors=errors,
             kwargs=kwargs,
         )
 
diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py
index 0f271e9d..f4ba9d4f 100644
--- a/xarray/core/dataset.py
+++ b/xarray/core/dataset.py
@@ -8631,6 +8631,7 @@ class Dataset(
         p0: dict[str, float | DataArray] | None = None,
         bounds: dict[str, tuple[float | DataArray, float | DataArray]] | None = None,
         param_names: Sequence[str] | None = None,
+        errors: ErrorOptions = "raise",
         kwargs: dict[str, Any] | None = None,
     ) -> T_Dataset:
         """
@@ -8675,6 +8676,10 @@ class Dataset(
             this will be automatically determined by arguments of `func`. `param_names`
             should be manually supplied when fitting a function that takes a variable
             number of parameters.
+        errors : {"raise", "ignore"}, default: "raise"
+            If 'raise', any errors from the `scipy.optimize_curve_fit` optimization will
+            raise an exception. If 'ignore', the coefficients and covariances for the
+            coordinates where the fitting failed will be NaN.
         **kwargs : optional
             Additional keyword arguments to passed to scipy curve_fit.
 
@@ -8757,6 +8762,9 @@ class Dataset(
                             f"dimensions {preserved_dims}."
                         )
 
+        if errors not in ["raise", "ignore"]:
+            raise ValueError('errors must be either "raise" or "ignore"')
+
         # Broadcast all coords with each other
         coords_ = broadcast(*coords_)
         coords_ = [
@@ -8793,7 +8801,15 @@ class Dataset(
                     pcov = np.full([n_params, n_params], np.nan)
                     return popt, pcov
             x = np.squeeze(x)
-            popt, pcov = curve_fit(func, x, y, p0=p0_, bounds=(lb, ub), **kwargs)
+
+            try:
+                popt, pcov = curve_fit(func, x, y, p0=p0_, bounds=(lb, ub), **kwargs)
+            except RuntimeError:
+                if errors == "raise":
+                    raise
+                popt = np.full([n_params], np.nan)
+                pcov = np.full([n_params, n_params], np.nan)
+
             return popt, pcov
 
         result = type(self)()
diff --git a/xarray/core/dtypes.py b/xarray/core/dtypes.py
index f5a18428..7ac342e3 100644
--- a/xarray/core/dtypes.py
+++ b/xarray/core/dtypes.py
@@ -74,7 +74,10 @@ def maybe_promote(dtype):
     else:
         dtype = object
         fill_value = np.nan
-    return np.dtype(dtype), fill_value
+
+    dtype = np.dtype(dtype)
+    fill_value = dtype.type(fill_value)
+    return dtype, fill_value
 
 
 NAT_TYPES = {np.datetime64("NaT").dtype, np.timedelta64("NaT").dtype}
diff --git a/xarray/core/groupby.py b/xarray/core/groupby.py
index 1bda5568..2a4465dc 100644
--- a/xarray/core/groupby.py
+++ b/xarray/core/groupby.py
@@ -626,6 +626,7 @@ def _resolve_group(obj: T_Xarray, group: T_Group | Hashable) -> T_Group:
         (group_dim,) = group.dims
         if len(group) != obj.sizes[group_dim]:
             raise ValueError(error_msg)
+        newgroup = DataArray(group)
 
     else:
         if not hashable(group):


Test patch:
diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py
index 190adab3..e0a2262a 100644
--- a/xarray/tests/test_backends.py
+++ b/xarray/tests/test_backends.py
@@ -46,6 +46,7 @@ from xarray.backends.netCDF4_ import (
 )
 from xarray.backends.pydap_ import PydapDataStore
 from xarray.backends.scipy_ import ScipyBackendEntrypoint
+from xarray.coding.strings import check_vlen_dtype, create_vlen_dtype
 from xarray.coding.variables import SerializationWarning
 from xarray.conventions import encode_dataset_coordinates
 from xarray.core import indexing
@@ -859,6 +860,20 @@ class CFEncodedBase(DatasetIOBase):
             with self.roundtrip(original) as actual:
                 assert_identical(expected, actual)
 
+    def test_roundtrip_empty_vlen_string_array(self) -> None:
+        # checks preserving vlen dtype for empty arrays GH7862
+        dtype = create_vlen_dtype(str)
+        original = Dataset({"a": np.array([], dtype=dtype)})
+        assert check_vlen_dtype(original["a"].dtype) == str
+        with self.roundtrip(original) as actual:
+            assert_identical(original, actual)
+            assert object == actual["a"].dtype
+            assert actual["a"].dtype == original["a"].dtype
+            # only check metadata for capable backends
+            # eg. NETCDF3 based backends do not roundtrip metadata
+            if actual["a"].dtype.metadata is not None:
+                assert check_vlen_dtype(actual["a"].dtype) == str
+
     @pytest.mark.parametrize(
         "decoded_fn, encoded_fn",
         [
diff --git a/xarray/tests/test_coding_strings.py b/xarray/tests/test_coding_strings.py
index cb9595f4..0c9f67e7 100644
--- a/xarray/tests/test_coding_strings.py
+++ b/xarray/tests/test_coding_strings.py
@@ -32,6 +32,10 @@ def test_vlen_dtype() -> None:
     assert strings.is_bytes_dtype(dtype)
     assert strings.check_vlen_dtype(dtype) is bytes
 
+    # check h5py variant ("vlen")
+    dtype = np.dtype("O", metadata={"vlen": str})  # type: ignore[call-overload]
+    assert strings.check_vlen_dtype(dtype) is str
+
     assert strings.check_vlen_dtype(np.dtype(object)) is None
 
 
diff --git a/xarray/tests/test_conventions.py b/xarray/tests/test_conventions.py
index acdf9c88..424b7db5 100644
--- a/xarray/tests/test_conventions.py
+++ b/xarray/tests/test_conventions.py
@@ -487,3 +487,18 @@ def test_decode_cf_error_includes_variable_name():
     ds = Dataset({"invalid": ([], 1e36, {"units": "days since 2000-01-01"})})
     with pytest.raises(ValueError, match="Failed to decode variable 'invalid'"):
         decode_cf(ds)
+
+
+def test_encode_cf_variable_with_vlen_dtype() -> None:
+    v = Variable(
+        ["x"], np.array(["a", "b"], dtype=coding.strings.create_vlen_dtype(str))
+    )
+    encoded_v = conventions.encode_cf_variable(v)
+    assert encoded_v.data.dtype.kind == "O"
+    assert coding.strings.check_vlen_dtype(encoded_v.data.dtype) == str
+
+    # empty array
+    v = Variable(["x"], np.array([], dtype=coding.strings.create_vlen_dtype(str)))
+    encoded_v = conventions.encode_cf_variable(v)
+    assert encoded_v.data.dtype.kind == "O"
+    assert coding.strings.check_vlen_dtype(encoded_v.data.dtype) == str
diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py
index e3138a32..cee5afa5 100644
--- a/xarray/tests/test_dataarray.py
+++ b/xarray/tests/test_dataarray.py
@@ -1698,6 +1698,19 @@ class TestDataArray:
         assert_identical(expected, actual)
         assert actual.dtype == expected.dtype
 
+    def test_reindex_empty_array_dtype(self) -> None:
+        # Dtype of reindex result should match dtype of the original DataArray.
+        # See GH issue #7299
+        x = xr.DataArray([], dims=("x",), coords={"x": []}).astype("float32")
+        y = x.reindex(x=[1.0, 2.0])
+
+        assert (
+            x.dtype == y.dtype
+        ), "Dtype of reindexed DataArray should match dtype of the original DataArray"
+        assert (
+            y.dtype == np.float32
+        ), "Dtype of reindexed DataArray should remain float32"
+
     def test_rename(self) -> None:
         da = xr.DataArray(
             [1, 2, 3], dims="dim", name="name", coords={"coord": ("dim", [5, 6, 7])}
@@ -4571,6 +4584,48 @@ class TestDataArray:
                 bounds={"a": (0, DataArray([1], coords={"foo": [1]}))},
             )
 
+    @requires_scipy
+    @pytest.mark.parametrize("use_dask", [True, False])
+    def test_curvefit_ignore_errors(self, use_dask: bool) -> None:
+        if use_dask and not has_dask:
+            pytest.skip("requires dask")
+
+        # nonsense function to make the optimization fail
+        def line(x, a, b):
+            if a > 10:
+                return 0
+            return a * x + b
+
+        da = DataArray(
+            [[1, 3, 5], [0, 20, 40]],
+            coords={"i": [1, 2], "x": [0.0, 1.0, 2.0]},
+        )
+
+        if use_dask:
+            da = da.chunk({"i": 1})
+
+        expected = DataArray(
+            [[2, 1], [np.nan, np.nan]], coords={"i": [1, 2], "param": ["a", "b"]}
+        )
+
+        with pytest.raises(RuntimeError, match="calls to function has reached maxfev"):
+            da.curvefit(
+                coords="x",
+                func=line,
+                # limit maximum number of calls so the optimization fails
+                kwargs=dict(maxfev=5),
+            ).compute()  # have to compute to raise the error
+
+        fit = da.curvefit(
+            coords="x",
+            func=line,
+            errors="ignore",
+            # limit maximum number of calls so the optimization fails
+            kwargs=dict(maxfev=5),
+        ).compute()
+
+        assert_allclose(fit.curvefit_coefficients, expected)
+
 
 class TestReduce:
     @pytest.fixture(autouse=True)
diff --git a/xarray/tests/test_groupby.py b/xarray/tests/test_groupby.py
index 8a93cf93..79b18cf1 100644
--- a/xarray/tests/test_groupby.py
+++ b/xarray/tests/test_groupby.py
@@ -137,6 +137,18 @@ def test_groupby_input_mutation() -> None:
     assert_identical(array, array_copy)  # should not modify inputs
 
 
+@pytest.mark.parametrize("use_flox", [True, False])
+def test_groupby_indexvariable(use_flox: bool) -> None:
+    # regression test for GH7919
+    array = xr.DataArray([1, 2, 3], [("x", [2, 2, 1])])
+    iv = xr.IndexVariable(dims="x", data=pd.Index(array.x.values))
+    with xr.set_options(use_flox=use_flox):
+        actual = array.groupby(iv).sum()
+    actual = array.groupby(iv).sum()
+    expected = xr.DataArray([3, 3], [("x", [1, 2])])
+    assert_identical(expected, actual)
+
+
 @pytest.mark.parametrize(
     "obj",
     [