Skip to content

Convert pandas str indexes to numpy StringDType consistently #11785

Open

Description

@headtr1ck

What is your issue?

Follow-up to #11322. With pandas 3, a default string index (StringDtype(na_value=nan)) only becomes a numpy StringDType coordinate when it's passed as the dim of concat. Everywhere else it becomes object:

import pandas as pd
import xarray as xr

idx = pd.Index(["a", "b"], name="x")
db = xr.DataArray(1)

xr.DataArray([1, 2], coords={"x": idx}, dims="x").x.dtype  # object
xr.Dataset(coords={"x": idx}).x.dtype  # object
xr.DataArray([1, 2], dims="x").assign_coords(x=idx).x.dtype  # object
pd.Series([1, 2], index=idx).to_xarray().x.dtype  # object
xr.concat([db, db], dim=xr.Variable("x", idx)).x.dtype  # object
xr.concat([db, db], dim=idx).x.dtype  # StringDType(na_object=nan)

(xarray main, pandas 3.0.6, numpy 2.5.4)

The reason is that PandasIndexingAdapter only maps pd.StringDtype to np.dtypes.StringDType when the dtype is passed explicitly (#11102), and only the concat dim path in _calc_concat_dim_index does that. In all other cases PandasIndexingAdapter falls back to get_valid_numpy_dtype (xarray/core/utils.py), which returns object for any non-numpy dtype.

As discussed in #11322 (comment), the plan is to use StringDType everywhere, following #11098. Since the minimum supported numpy version is now 2.2, StringDType is always available, so no object fallback for older numpy versions is needed. That probably means:

  • Map pd.StringDtype to np.dtypes.StringDType(na_object=dtype.na_value) in get_valid_numpy_dtype (or wherever the default coord dtype is picked), so that PandasIndex and PandasIndexingAdapter don't need the dtype passed explicitly.
  • Put this mapping in a single shared helper; it currently exists in PandasIndexingAdapter.__init__ and, after Fix concat failure with pandas StringDtype indexes (#11317) #11322, also in _calc_concat_dim_index.
  • Decide whether non-index variables should follow, e.g. a pandas str column in Dataset.from_dataframe, which currently also becomes object.

Activity

  1. headtr1ck commented on Oct 22, 2026

    @headtr1ck
    CollaboratorAuthor

    If StringDType becomes the default for pandas strings, the .str accessor needs some work too. I compared every accessor method on the same data as <U and as StringDType(). Without missing values, almost everything gives the same values, and string-returning methods keep StringDType. These are the gaps (xarray main, numpy 2.5.4):

    import numpy as np
    import xarray as xr
    
    sdt = np.dtypes.StringDType()
    da = xr.DataArray(np.array(["abc", "a_b"], dtype=sdt), dims="x")
    u = xr.DataArray(["abc", "a_b"], dims="x")
    
    da.str.extract(r"(.)(.)", dim="g")
    # AttributeError: 'str' object has no attribute 'dtype'
    
    u.str.contains(da)  # same for replace, cat, + with a StringDType argument
    # TypeError: cannot cast dtype StringDType() to <class 'numpy.dtypes.StrDType'>.
    
    nan_sdt = np.dtypes.StringDType(na_object=np.nan)
    xr.DataArray(np.array(["a", np.nan], dtype=nan_sdt)).str.upper()
    # AttributeError: 'float' object has no attribute 'upper'
    
    xr.DataArray(np.array(["a_b"], dtype=nan_sdt)).str.split(dim="p", sep="_").dtype
    # StringDType()  (na_object is lost)
    • extract with dim: elements of a StringDType array are plain Python str, so val.dtype in _get_res_multi fails. Using dtype=object there would be enough, since the result is cast afterwards anyway.
    • Mixed <U/StringDType: _stringify casts arguments with astype(self._obj.dtype.kind), i.e. astype("U"), which numpy doesn't allow for StringDType.
    • na_object is lost: several methods cast their result with astype(self._obj.dtype.kind) ("T"), which gives the default StringDType() (e.g. cat/+, split, partition, extract).
    • Missing values: with na_object=np.nan (what pandas 3 str data maps to) or None, almost every method fails. Object arrays with NaN fail the same way, so the accessor never supported missing values, but with StringDType it becomes much more common.
    • test_accessor_str.py only covers np.str_ and np.bytes_.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions