From 1f5e58e5d0d783dd25483f7c1c0b430ec73cde47 Mon Sep 17 00:00:00 2001 From: agu2347 Date: Tue, 21 Jul 2026 16:40:22 +0000 Subject: [PATCH] Preserve object dtype on full reduction (e.g. .sum()) of object arrays Summing (or otherwise fully reducing over all axes) an object-dtype DataArray/Variable converted the result to a numpy fixed-width string/unicode dtype instead of preserving the original object dtype, e.g. summing an all-string object array produced dtype '`_. +- Fixed a full reduction (e.g. ``.sum()`` with no remaining dimensions) over + an object-dtype array converting the result to a numpy fixed-width + string/unicode dtype instead of preserving the original object dtype + (:issue:`5024`). Documentation diff --git a/xarray/namedarray/core.py b/xarray/namedarray/core.py index c535fcb0bc4..0ec6ce01577 100644 --- a/xarray/namedarray/core.py +++ b/xarray/namedarray/core.py @@ -929,6 +929,21 @@ def reduce( else: data = func(self.data, **kwargs) + if self.dtype == object and not hasattr(data, "dtype"): + # A full reduction (e.g. .sum() with no remaining axes) + # over an object-dtype array can return a raw Python + # scalar rather than a 0-d array (e.g. np.sum() on an + # array of strings returns a plain str). Left as-is, + # this raw scalar would later be wrapped by + # from_array()'s generic np.asarray(data) fallback, + # which infers a dtype from the scalar itself (e.g. + # numpy unicode dtype for a str) instead of preserving + # the original object dtype. Explicitly wrap it in a + # 0-d object array here, while we still know the + # original dtype, using the same helper already used + # for tuple inputs in from_array(). See GH #5024. + data = to_0d_object_array(data) + if getattr(data, "shape", ()) == self.shape: dims = self.dims else: diff --git a/xarray/tests/test_variable.py b/xarray/tests/test_variable.py index b493d943bb2..d4ea52b5ff2 100644 --- a/xarray/tests/test_variable.py +++ b/xarray/tests/test_variable.py @@ -1888,6 +1888,30 @@ def test_reduce(self): with pytest.raises(ValueError, match=r"cannot supply both"): v.mean(dim="x", axis=0) + def test_reduce_keeps_object_dtype_on_full_reduction(self): + """ + Regression test for + https://github.com/pydata/xarray/issues/5024 + + Summing (or otherwise fully reducing) an object-dtype Variable + over all axes must preserve the object dtype, not silently + convert it to a numpy fixed-width string/unicode dtype. + """ + v = Variable(["x", "y"], np.full((3, 3), "a", dtype=object)) + + full_reduction = v.sum() + assert full_reduction.dtype == object + assert full_reduction.item() == "a" * 9 + + # Partial reduction already preserved dtype correctly before + # this fix; confirm it still does. + partial_reduction = v.sum("x") + assert partial_reduction.dtype == object + + # Sanity check: numeric full reductions are unaffected. + numeric = Variable(["x", "y"], np.arange(9, dtype=float).reshape(3, 3)) + assert numeric.sum().dtype == np.float64 + @requires_bottleneck @pytest.mark.parametrize("compute_backend", ["bottleneck"], indirect=True) def test_reduce_use_bottleneck(self, monkeypatch, compute_backend):