Skip to content

BUG: Fix Index.droplevel() stub — use Sequence[Never] to allow only droplevel([]) (GH#1678)#1732

Merged
loicdiridollou merged 8 commits into
pandas-dev:mainfrom
tinezivic:fix/index-droplevel-not-permitted
Apr 25, 2026
Merged

BUG: Fix Index.droplevel() stub — use Sequence[Never] to allow only droplevel([]) (GH#1678)#1732
loicdiridollou merged 8 commits into
pandas-dev:mainfrom
tinezivic:fix/index-droplevel-not-permitted

Conversation

@tinezivic
Copy link
Copy Markdown
Contributor

@tinezivic tinezivic commented Apr 22, 2026

Problem

Index.droplevel() was defined in the Index base stub, but calling it on a plain Index with any non-empty argument always raises ValueError at runtime. A plain Index has exactly one level — you cannot remove it.

Type checkers accepted idx.droplevel(0) without complaint, misleading users into thinking this call is safe.

Fix

Use Sequence[Never] as the parameter type for Index.droplevel():

# pandas-stubs/core/indexes/base.pyi
def droplevel(self, level: Sequence[Never]) -> Self: ...

Sequence[Never] means "a sequence with no possible elements" — in practice, only the empty list [] satisfies this type. Both mypy and pyright (verified with actual tests) correctly:

  • allow idx.droplevel([]) — which is a documented no-op at runtime
  • reject idx.droplevel(), idx.droplevel(0), idx.droplevel([0]), idx.droplevel("name") — all of which raise ValueError

This is more precise than deleting the method entirely, because droplevel([]) is documented behavior.

Changes

  • base.pyi: add droplevel(level: Sequence[Never]) -> Self
  • multi.pyi: droplevel unchanged; # type: ignore[override] stays because the return type MultiIndex | Index is wider than Self (needed for correctness, unrelated to our change)
  • tests/indexes/test_indexes.py: add idx.droplevel([]) as a valid usage; expand TYPE_CHECKING_INVALID_USAGE block with four invalid cases (droplevel(), droplevel(0), droplevel([0]), droplevel("name"))

Why Sequence[Never] works

Both mypy and pyright treat [] as assignable to Sequence[Never] (an empty sequence has no elements, so the element type constraint is vacuously satisfied), but reject any non-empty sequence. This is the most accurate annotation for the actual runtime contract on a plain Index.

…alid (GH#1678)

Index.droplevel() always raises ValueError at runtime because a plain
Index has exactly one level. Only MultiIndex.droplevel() is meaningful.

Remove droplevel() from the Index base stub so type checkers (pyright/mypy)
report an error when users call it on a plain Index, prompting them to use
cast(MultiIndex, idx).droplevel(...) explicitly.

- Remove droplevel() from pandas-stubs/core/indexes/base.pyi
- Drop now-unnecessary # type: ignore[override] comment on MultiIndex.droplevel()
- Update test: move Index.droplevel() to TYPE_CHECKING_INVALID_USAGE block
Copy link
Copy Markdown
Member

@loicdiridollou loicdiridollou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pd.Index().droplevel([]) does not raise an error and I believe should be kept, @Dr-Irv what do you think?

Comment thread tests/indexes/test_indexes.py
@Dr-Irv
Copy link
Copy Markdown
Collaborator

Dr-Irv commented Apr 24, 2026

pd.Index().droplevel([]) does not raise an error and I believe should be kept, @Dr-Irv what do you think?

Well, I'm the one that created the issue, and that means it happened in code my team was working on, so I'm fine with having the type checker say that pd.Index().droplevel([]) is invalid.

@loicdiridollou
Copy link
Copy Markdown
Member

Sorry not clear, my argument was that pd.Index().droplevel([]) is a valid code but pd.Index().droplevel([0]) is not so my point was if we should type Sequence[Never] as valid and leave the rest as is in the PR

@Dr-Irv
Copy link
Copy Markdown
Collaborator

Dr-Irv commented Apr 24, 2026

Sorry not clear, my argument was that pd.Index().droplevel([]) is a valid code but pd.Index().droplevel([0]) is not so my point was if we should type Sequence[Never] as valid and leave the rest as is in the PR

From what I've read, type checkers are not able to distinguish between empty sequences and non-empty sequences. But if you can get it to work, go ahead.

… while rejecting all other args (GH#1678)

droplevel() on a plain Index always raises ValueError unless called with an
empty sequence. The previous fix (deleting the stub entirely) was overly strict
because droplevel([]) is a documented no-op that works at runtime.

Use Sequence[Never] instead — type checkers (mypy and pyright both verified)
correctly allow droplevel([]) while rejecting droplevel(0), droplevel([0]),
droplevel('name'), etc.

The type: ignore[override] on MultiIndex.droplevel stays — it is needed because
the return type MultiIndex | Index is wider than Self (what the parent stub
declares), not because of argument types.
@tinezivic tinezivic changed the title BUG: Remove Index.droplevel() stub — only MultiIndex.droplevel() is valid (GH#1678) BUG: Fix Index.droplevel() stub — use Sequence[Never] to allow only droplevel([]) (GH#1678) Apr 24, 2026
@tinezivic
Copy link
Copy Markdown
Contributor Author

Hey @loicdiridollou — you were right, I tested it and Sequence[Never] actually works here. Both mypy and pyright allow droplevel([]) and reject droplevel(0), droplevel([0]), droplevel("name") — exactly the behavior we want.

I updated the PR: Index.droplevel is back in base.pyi with Sequence[Never] as the parameter type, and the tests now cover both the valid no-op case (droplevel([])) and the three invalid cases in TYPE_CHECKING_INVALID_USAGE.

The # type: ignore[override] on MultiIndex.droplevel is also back — turns out it was never about argument types, it's needed because the return type MultiIndex | Index is wider than Self. Sorry for dropping it earlier without understanding why it was there.

Comment thread pandas-stubs/core/indexes/base.pyi Outdated
Comment thread tests/indexes/test_indexes.py Outdated
@tinezivic
Copy link
Copy Markdown
Contributor Author

Done, removed all comments.

Comment thread tests/indexes/test_indexes.py Outdated
Copy link
Copy Markdown
Member

@loicdiridollou loicdiridollou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On minor thing, rest looks good, check CI I kicked it to make sure it passes or not

Comment thread pandas-stubs/core/indexes/multi.pyi Outdated
Comment thread pandas-stubs/core/indexes/base.pyi Outdated
Comment thread tests/indexes/test_indexes.py
tinezivic and others added 3 commits April 25, 2026 17:00
Co-authored-by: Yi-Fan Wang <cmp0xff@users.noreply.github.com>
Co-authored-by: Yi-Fan Wang <cmp0xff@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@cmp0xff cmp0xff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. I would not move droplevel from L434 to L454, but it's not a bit deal.

I'll hand over to @loicdiridollou to merge.

Copy link
Copy Markdown
Member

@loicdiridollou loicdiridollou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good, thanks for your contribution @tinezivic !

@loicdiridollou loicdiridollou merged commit a7c2115 into pandas-dev:main Apr 25, 2026
14 checks passed
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.

Index.droplevel() should not be permitted

4 participants