From a303a16f4f94c54e736187a2bc25498d3105b514 Mon Sep 17 00:00:00 2001 From: Kayvan Zahiri Date: Tue, 11 Aug 2026 14:33:19 -0700 Subject: [PATCH 1/2] Raise AlignmentError when alignment would reorder an already-aligned index An index that requires no reindexing is equal across all objects, so it pins its dimensions in place. It produces no re-indexer, which meant the conflict check in _get_dim_pos_indexers never saw it, and a second index sharing the same dimension could reorder it silently. The mismatched index was then used to combine the data. Record the dimensions pinned by non-reindexed indexes and reject any re-indexer that would reorder one of them. A re-indexer that is the identity still passes, so alignment that only differs on unindexed dimension sizes is unaffected. Closes #10714 --- doc/whats-new.rst | 6 ++++++ xarray/structure/alignment.py | 28 ++++++++++++++++++++++++++++ xarray/tests/test_dataset.py | 17 +++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 70aef4acf3e..760cab51552 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -63,6 +63,12 @@ Bug Fixes - :py:func:`polyval` now propagates ``NaN`` for ``NaT`` entries in ``timedelta64`` coordinates instead of returning a large sentinel value (:issue:`11462`). By `Dipak Chaudhari `_. +- :py:func:`align` now raises ``AlignmentError`` when one index would reorder a + dimension that another, already aligned, index shares. Previously an index + requiring no reindexing was skipped by the conflict check, so conflicting + indexes aligned silently and the mismatched index was used to combine the data + (:issue:`10714`). + By `Kayvan Zahiri `_. Documentation diff --git a/xarray/structure/alignment.py b/xarray/structure/alignment.py index f5d61e63409..e0eb7b0811a 100644 --- a/xarray/structure/alignment.py +++ b/xarray/structure/alignment.py @@ -550,6 +550,21 @@ def _get_dim_pos_indexers( dim_pos_indexers: dict[Hashable, Any] = {} dim_index: dict[Hashable, Index] = {} + # An index that needs no reindexing is equal across all objects, so it pins + # its dimensions in place. It never produces an indexer of its own, which + # means the conflict check below would otherwise never see it and a second + # index sharing the same dimension could silently reorder it. + unchanged_dim_index: dict[Hashable, Index] = {} + for key in self.aligned_indexes: + obj_idx = matching_indexes.get(key) + if obj_idx is not None and not self.reindex[key]: + for dim in { + d + for var in self.aligned_index_vars[key].values() + for d in var.dims + }: + unchanged_dim_index.setdefault(dim, obj_idx) + for key, aligned_idx in self.aligned_indexes.items(): obj_idx = matching_indexes.get(key) if obj_idx is not None and self.reindex[key]: @@ -562,6 +577,19 @@ def _get_dim_pos_indexers( "wrong results returned by the `reindex_like` method of this index:\n" f"{obj_idx!r}" ) + idxer_arr = np.asarray(idxer) + reorders = not ( + idxer_arr.ndim == 1 + and np.array_equal(idxer_arr, np.arange(idxer_arr.size)) + ) + if dim in unchanged_dim_index and reorders: + raise AlignmentError( + f"cannot reindex or align along dimension {dim!r} because " + "it would reorder another index that is already aligned along " + "that dimension\n" + f"first index: {obj_idx!r}\n" + f"second index: {unchanged_dim_index[dim]!r}\n" + ) if dim in dim_pos_indexers and not np.array_equal( idxer, dim_pos_indexers[dim] ): diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index a7178bba512..f54f223f9ec 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -2729,6 +2729,23 @@ def test_align_multiple_indexes_common_dim(self) -> None: with pytest.raises(AlignmentError, match=r".*conflicting re-indexers"): align(a, c) + def test_align_multiple_indexes_common_dim_no_reindex(self) -> None: + # An index that needs no reindexing produces no re-indexer, so it used to + # escape the conflict check entirely and a second index sharing its + # dimension could silently reorder it. See GH10714. + a = Dataset(coords={"x": [1, 2, 3], "xb": ("x", [4, 5, 6])}).set_xindex("xb") + # "x" is equal in both and needs no reindexing; only "xb" conflicts + b = Dataset(coords={"x": [1, 2, 3], "xb": ("x", [4, 6, 5])}).set_xindex("xb") + + with pytest.raises(AlignmentError, match=r".*would reorder another index"): + align(a, b) + + # reordering a dimension is still fine when nothing else pins it + d = Dataset(coords={"x": [3, 1, 2], "xb": ("x", [6, 4, 5])}).set_xindex("xb") + (a2, d2) = align(a, d) + assert_identical(a2, a, check_default_indexes=False) + assert_identical(d2, a, check_default_indexes=False) + def test_align_conflicting_indexes(self) -> None: class CustomIndex(PandasIndex): ... From 59b5f87a025877d4377ef057c8e3c2627b4f31b4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:36:22 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- xarray/structure/alignment.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/xarray/structure/alignment.py b/xarray/structure/alignment.py index e0eb7b0811a..6acdeb2bd85 100644 --- a/xarray/structure/alignment.py +++ b/xarray/structure/alignment.py @@ -559,9 +559,7 @@ def _get_dim_pos_indexers( obj_idx = matching_indexes.get(key) if obj_idx is not None and not self.reindex[key]: for dim in { - d - for var in self.aligned_index_vars[key].values() - for d in var.dims + d for var in self.aligned_index_vars[key].values() for d in var.dims }: unchanged_dim_index.setdefault(dim, obj_idx)