From 49eb00afb7e9f0ad6ef5c9bda1a0ff75e5864ae0 Mon Sep 17 00:00:00 2001 From: Kropiunig <48442031+Kropiunig@users.noreply.github.com> Date: Thu, 27 Aug 2026 01:18:48 +0200 Subject: [PATCH 1/3] fix: make str.replace with n=0 perform no replacements The regex branch of StringAccessor.replace passed count=max(n, 0) to re.sub, where count=0 means 'replace every occurrence'. That collapsed n=0 onto n=-1, so asking for zero replacements silently replaced all of them, while the regex=False branch (str.replace) honoured n=0 correctly. Special-case n=0 so both branches match the documented behaviour that n is the number of replacements to make from the start and only -1 means 'replace all'. --- xarray/core/accessor_str.py | 6 ++++-- xarray/tests/test_accessor_str.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/xarray/core/accessor_str.py b/xarray/core/accessor_str.py index 0699fbdd5b1..b2f6fd4a897 100644 --- a/xarray/core/accessor_str.py +++ b/xarray/core/accessor_str.py @@ -1944,8 +1944,10 @@ def replace( if regex: pat = self._re_compile(pat=pat, flags=flags, case=case) - func = lambda x, ipat, irepl, i_n: ipat.sub( - repl=irepl, string=x, count=max(i_n, 0) + # ``re.sub`` interprets ``count=0`` as "replace every occurrence", + # so ``n=0`` has to be special-cased to mean "replace nothing". + func = lambda x, ipat, irepl, i_n: ( + x if i_n == 0 else ipat.sub(repl=irepl, string=x, count=max(i_n, 0)) ) else: pat = self._stringify(pat) diff --git a/xarray/tests/test_accessor_str.py b/xarray/tests/test_accessor_str.py index 0741fa364fc..5f5849bc9de 100644 --- a/xarray/tests/test_accessor_str.py +++ b/xarray/tests/test_accessor_str.py @@ -381,6 +381,28 @@ def test_replace(dtype) -> None: assert_equal(result, expected) +def test_replace_n_zero(dtype) -> None: + # ``n=0`` means "make no replacements", for regex and literal patterns alike + values = xr.DataArray(["fooBAD__barBAD"], dims=["x"]).astype(dtype) + + result = values.str.replace("BAD[_]*", "", n=0) + assert result.dtype == values.dtype + assert_equal(result, values) + + result = values.str.replace("BAD", "", n=0, regex=False) + assert result.dtype == values.dtype + assert_equal(result, values) + + # ``n`` is broadcast, so a single zero must not spill over to its neighbours + n = xr.DataArray([0, 1, -1], dims=["y"]) + result = values.str.replace("BAD[_]*", "", n=n) + expected = xr.DataArray( + [["fooBAD__barBAD", "foobarBAD", "foobar"]], dims=["x", "y"] + ).astype(dtype) + assert result.dtype == expected.dtype + assert_equal(result, expected) + + def test_replace_callable() -> None: values = xr.DataArray(["fooBAD__barBAD"]) From e2ec5fb9c7b20947d0b0b506c976a2558f7682ba Mon Sep 17 00:00:00 2001 From: Kropiunig <48442031+Kropiunig@users.noreply.github.com> Date: Thu, 27 Aug 2026 01:22:59 +0200 Subject: [PATCH 2/3] docs: add whats-new entry for str.replace n=0 fix --- doc/whats-new.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index d1505bfa081..d69deefdd93 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -53,6 +53,11 @@ Deprecations Bug Fixes ~~~~~~~~~ +- Fixed ``DataArray.str.replace`` replacing every occurrence instead of none when + ``n=0``. ``re.sub`` treats ``count=0`` as "replace all", so the regex code path + collapsed ``n=0`` onto ``n=-1``, while the ``regex=False`` path already handled + ``n=0`` correctly. + By `Alexander Kropiunig `_. - Fix async zarr tests using ``wraps`` with ``autospec=True`` on async methods, which caused ``AsyncMock`` objects to leak through instead of real array data (:pull:`11232`). From 3c4083334497cdac6f10e7d64c65d93e5ff20b3d Mon Sep 17 00:00:00 2001 From: Kropiunig <48442031+Kropiunig@users.noreply.github.com> Date: Thu, 27 Aug 2026 01:34:21 +0200 Subject: [PATCH 3/3] docs: reference PR number in whats-new entry --- doc/whats-new.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index d69deefdd93..8c807f03dab 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -56,7 +56,7 @@ Bug Fixes - Fixed ``DataArray.str.replace`` replacing every occurrence instead of none when ``n=0``. ``re.sub`` treats ``count=0`` as "replace all", so the regex code path collapsed ``n=0`` onto ``n=-1``, while the ``regex=False`` path already handled - ``n=0`` correctly. + ``n=0`` correctly (:pull:`11545`). By `Alexander Kropiunig `_. - Fix async zarr tests using ``wraps`` with ``autospec=True`` on async methods, which caused ``AsyncMock`` objects to leak through instead of real array data