From 13ca20562ad24d6c8547c6201c8972932ac01c10 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 da7259da069..cb999871ffc 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -120,6 +120,12 @@ Bug Fixes - Fixed a bug that caused rechunking a multi-dimensional cftime array along a subset of its dimensions to raise an error (:issue:`11567`, :pull:`11576`). By `Spencer Clark `_. +- :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 `_. .. _`pandas-dev/pandas#64793`: https://github.com/pandas-dev/pandas/pull/64793 diff --git a/xarray/structure/alignment.py b/xarray/structure/alignment.py index 1a61afd3e31..e8f98b975b3 100644 --- a/xarray/structure/alignment.py +++ b/xarray/structure/alignment.py @@ -561,6 +561,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]: @@ -573,6 +588,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 883a6dc89b7..460523b75ad 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 99a4887cdb7d67efb5b27930a1a033b057cf5fc9 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 e8f98b975b3..46f4a016eaf 100644 --- a/xarray/structure/alignment.py +++ b/xarray/structure/alignment.py @@ -570,9 +570,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)