Repository: pydata/xarray

Issue:
# BUG: normalize_indexer breaks _decompose_slice

### What happened?

With `xarray==2025.12.0`, the rioxarray tests started to fail:

https://github.com/corteva/rioxarray/actions/runs/19997389961/job/57347087949

The issue was traced back to this change #10948.

### What did you expect to happen?

No error.

### Minimal Complete Verifiable Example

```Python
# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "xarray[complete]@git+https://github.com/pydata/xarray.git@main",
# ]
# ///
#
# This script automatically imports the development branch of xarray to check for issues.
# Please delete this header if you have _not_ tested this script with `uv run`!

import xarray as xr
xr.show_versions()
from xarray.core.indexing import normalize_indexer, _decompose_slice
print(_decompose_slice(slice(-1, None, -1), 8))
# (slice(0, 8, 1), slice(None, None, -1))
print(_decompose_slice(normalize_indexer(slice(-1, None, -1), 8), 8))
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#     _decompose_slice(normalize_indexer(slice(-1, None, -1), 8), 8)
#     ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#   File ".../lib/python3.14/site-packages/xarray/core/indexing.py", # line 1217, in _decompose_slice
#     exact_stop = range(start, stop, step)[-1]
#                  ~~~~~~~~~~~~~~~~~~~~~~~~^^^^
# IndexError: range object index out of range
```

### Steps to reproduce

_No response_

### 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.
- [x] Recent environment — the issue occurs with the latest version of xarray and its dependencies.

### Relevant log output

```Python
=================================== FAILURES ===================================
________________________________ test_indexing _________________________________

    def test_indexing():
...
               # minus-stepped slice
                ind = {"band": numpy.array([2, 1, 0]), "x": slice(-1, None, -1), "y": 0}
>               assert_allclose(expected.isel(**ind), actual.isel(**ind))
...
testenv/lib/python3.12/site-packages/xarray/core/indexing.py:1415: in _decompose_outer_indexer
    bk_slice, np_slice = _decompose_slice(k, s)
                         ^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

key = slice(7, -1, -1), size = 8

    def _decompose_slice(key: slice, size: int) -> tuple[slice, slice]:
            # determine stop precisely for step > 1 case
            # Use the range object to do the calculation
            # e.g. [98:2:-2] -> [98:3:-2]
>           exact_stop = range(start, stop, step)[-1]
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E           IndexError: range object index out of range
```

### Anything else we need to know?

_No response_

### Environment

<details>
https://github.com/corteva/rioxarray/actions/runs/19997389961/job/57347087949

xarray-2025.12.0
cftime-1.6.5
cloudpickle-3.1.2
dask-2025.11.0
fsspec-2025.12.0
netcdf4-1.7.3
</details>


Implementation patch:
diff --git a/xarray/core/indexing.py b/xarray/core/indexing.py
index 59c9807f588..bb12704e55c 100644
--- a/xarray/core/indexing.py
+++ b/xarray/core/indexing.py
@@ -269,8 +269,11 @@ def normalize_slice(sl: slice, size: int) -> slice:
     slice(0, 9, 1)
     >>> normalize_slice(slice(0, -1), 10)
     slice(0, 9, 1)
+    >>> normalize_slice(slice(None, None, -1), 10)
+    slice(9, None, -1)
     """
-    return slice(*sl.indices(size))
+    start, stop, step = sl.indices(size)
+    return slice(start, stop if stop >= 0 else None, step)
 
 
 def _expand_slice(slice_: slice, size: int) -> np.ndarray[Any, np.dtype[np.integer]]:
@@ -284,8 +287,8 @@ def _expand_slice(slice_: slice, size: int) -> np.ndarray[Any, np.dtype[np.integ
     >>> _expand_slice(slice(0, -1), 10)
     array([0, 1, 2, 3, 4, 5, 6, 7, 8])
     """
-    sl = normalize_slice(slice_, size)
-    return np.arange(sl.start, sl.stop, sl.step)
+    start, stop, step = slice_.indices(size)
+    return np.arange(start, stop, step)
 
 
 def slice_slice(old_slice: slice, applied_slice: slice, size: int) -> slice:
@@ -293,14 +296,14 @@ def slice_slice(old_slice: slice, applied_slice: slice, size: int) -> slice:
     index it with another slice to return a new slice equivalent to applying
     the slices sequentially
     """
-    old_slice = normalize_slice(old_slice, size)
+    old_slice = slice(*old_slice.indices(size))
 
     size_after_old_slice = len(range(old_slice.start, old_slice.stop, old_slice.step))
     if size_after_old_slice == 0:
         # nothing left after applying first slice
         return slice(0)
 
-    applied_slice = normalize_slice(applied_slice, size_after_old_slice)
+    applied_slice = slice(*applied_slice.indices(size_after_old_slice))
 
     start = old_slice.start + applied_slice.start * old_slice.step
     if start < 0:


Test patch:
diff --git a/xarray/tests/test_indexing.py b/xarray/tests/test_indexing.py
index fabfec4e038..6b0fcd2f59a 100644
--- a/xarray/tests/test_indexing.py
+++ b/xarray/tests/test_indexing.py
@@ -300,10 +300,11 @@ class TestLazyArray:
             (-1, 3, 2),
             (slice(None), 4, slice(0, 4, 1)),
             (slice(1, -3), 7, slice(1, 4, 1)),
+            (slice(None, None, -1), 8, slice(7, None, -1)),
             (np.array([-1, 3, -2]), 5, np.array([4, 3, 3])),
         ),
     )
-    def normalize_indexer(self, indexer, size, expected):
+    def test_normalize_indexer(self, indexer, size, expected):
         actual = indexing.normalize_indexer(indexer, size)
 
         if isinstance(expected, np.ndarray):