Skip to content

Raise AlignmentError when alignment would reorder an already-aligned index (closes #10714) - #11508

Open
Kayvan-Zahiri wants to merge 2 commits into
pydata:mainfrom
Kayvan-Zahiri:fix/align-unreindexed-index-conflict
Open

Raise AlignmentError when alignment would reorder an already-aligned index (closes #10714)#11508
Kayvan-Zahiri wants to merge 2 commits into
pydata:mainfrom
Kayvan-Zahiri:fix/align-unreindexed-index-conflict

Conversation

@Kayvan-Zahiri

@Kayvan-Zahiri Kayvan-Zahiri commented Aug 11, 2026

Copy link
Copy Markdown

Closes #10714.

The bug

align is documented to raise AlignmentError when objects carry conflicting
indexes, but the consistency check in _get_dim_pos_indexers only runs for
indexes that need reindexing:

if obj_idx is not None and self.reindex[key]:

_need_reindex returns False precisely when the matching indexes are already
equal, so such an index pins its dimension in place while producing no
re-indexer of its own. Nothing is ever registered for that dimension, so a second
index sharing it has nothing to conflict with and silently wins.

Using @abaarsma's MVCE, x is identical in both objects and xb is not:

ds1 = Dataset(coords={"x": [1, 2, 3], "xb": ("x", [4, 5, 6])}).set_xindex("xb")
ds2 = Dataset(coords={"x": [1, 2, 3], "xb": ("x", [4, 6, 5])}).set_xindex("xb")

align(ds1, ds2)   # no error

It is not only a missing error. ds2.xb is [4, 6, 5] going in and [4, 5, 6]
coming out: the conflicting index is silently rewritten and the mismatched index
is used to combine the data. align(ds1, ds3), where both indexes need
reindexing, raises correctly today, so the difference is purely whether the
second index happened to need reindexing.

The fix

Record the dimensions pinned by non-reindexed indexes, then reject any re-indexer
that would reorder one of them.

The identity check matters and is not redundant. _need_reindex can return True
for reasons other than index inequality, notably differing unindexed dimension
sizes, and in that case reindex_like returns an identity indexer that reorders
nothing. Treating "needs reindexing" as "conflicts" would break those cases, so
the guard is specifically "would reorder", not "would reindex".

dim_pos_indexers is unchanged, so the actual reindexing behaviour is untouched.
The only difference is that a previously silent case now raises.

Verification

  • The MVCE now raises; align(ds1, ds3) still raises; align(ds1, ds1) and
    align(ds1) are unaffected.
  • Reordering a dimension is still legal when nothing else pins it, which the new
    test asserts explicitly rather than leaving to inference.
  • test_indexes.py, test_concat.py, test_merge.py: 300 passed, 2 skipped.
  • Every file containing an align test: 1279 passed, 433 skipped, 14 xfailed,
    4 xpassed. Two failures in test_chunk_by_season_resampler are an ImportError
    for cftime and reproduce identically on an unmodified checkout, so they are
    environmental rather than caused by this change.
  • The new test fails with DID NOT RAISE on main and passes with the fix.

Credit

The diagnosis is @abaarsma's, including pinpointing the exact branch in
_get_dim_pos_indexers, and @dcherian confirmed it. Nobody had opened a PR, so I
have written it up with a regression test and a whatsnew entry.

AI Disclosure

  • This PR contains AI-generated content.
    • I have tested any AI-generated content in my PR.

    • I take responsibility for any AI-generated content in my PR.

      Tools: Claude (Claude Code), used to diagnose the root cause, draft the fix and the
      regression test, and draft this description.

Kayvan-Zahiri and others added 2 commits August 11, 2026 14:33
…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 pydata#10714
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

align skips alignment checks for indexes that are not reindexed

1 participant