Skip to content

Fix self in identity alias - #22182

Open
Moussa-Mikhail wants to merge 2 commits into
python:masterfrom
Moussa-Mikhail:fix-self-in-identity-alias
Open

Moussa-Mikhail wants to merge 2 commits into
python:masterfrom
Moussa-Mikhail:fix-self-in-identity-alias

Conversation

@Moussa-Mikhail

Copy link
Copy Markdown

fixes #22181

This PR expands type aliases in annotations of self and cls so that is_expected_self_type can handle them like any other annotation. It also refactors some repetitive if conditions by early returning when they aren't true. Using mypy's own type substitution means type aliases with strange shapes (e.g. type Extra[T, S] = T, UnionSame[T] = T | T) work without special-casing them.

The code in this PR was written by Claude and reviewed by me.

@github-actions

This comment has been minimized.

Moussa-Mikhail and others added 2 commits October 11, 2026 20:33
…otation

is_expected_self_type() already unwraps Annotated[Self, ...] so a method
like "def m(self: Annotated[Self, ...])" is recognized as equivalent to a
bare "self: Self". A generic type alias that's equally transparent (e.g.
"type Ident[T] = T") had no equivalent handling, so "self: Ident[Self]"
was wrongly rejected with "Method cannot have explicit self annotation
and Self type".

Adds expand_type_alias_for_self_check(), which substitutes the alias's
own type parameters with the arguments it was referenced with (using
mypy's existing substitution, mypy.expandtype.expand_type) and recurses
is_expected_self_type() on the result -- the same way the existing
Annotated[...] case recurses on its one argument. This is handled in
both places an explicit self-type annotation is checked: the plain/
instance-method branch (self: Ident[Self]) and the classmethod branch,
which needs it in two different positions depending on how the alias and
type[...] are nested -- cls: type[Ident[Self]] unwraps through the
existing TypeType case first and lands in the plain branch, while
cls: Ident[type[Self]] needs the same handling in the classmethod
branch's own UnboundType case, recursing with is_classmethod=True
preserved -- same as the ANNOTATED_TYPE_NAMES case right next to it.

Using real substitution instead of hand-matching "is the target exactly
the alias's sole type parameter" also covers, without any extra code: an
alias with extra, unused type parameters, in any order (e.g. type
Extra[T, S] = T used as Extra[Self, int] or Extra[int, Self]); and a
target that only reduces to the expected shape once substituted (e.g.
type UnionSame[T] = T | T, which collapses once both T's become Self).
Self supplied for an alias's *unused* parameter is correctly still
rejected, since the substitution never looks at that slot.

Also drops the redundant "sym is not None" repeated on every branch below
the symbol lookup, in both the classmethod and instance-method cases, in
favor of a single early return when the lookup fails -- the control flow
in each branch already made the None case fall through to the same
result, so this part is a pure refactor with no behavior change.

expand_type_alias_for_self_check()'s args parameter takes UnboundType.args
directly, which mypy's own types.py types as a Sequence[Type] (stored
internally as a tuple), not a list -- declared as list[Type] at first,
which only surfaced as a real error under mypy's own self-check
(runtests.py self), not under ordinary testing. Fixed to Sequence[Type],
imported from collections.abc to match this module's existing convention
for Callable/Collection/Iterable/Iterator.

Verified: bare self: Self, Annotated[Self, ...], an explicit concrete
self-type, and a non-Self alias argument (Ident[int], which should and
does still raise a real self-type error) are all unaffected; both
classmethod orderings, extra-unused-parameter aliases (both orders), and
the union-collapse case all work; Self in an alias's unused slot still
correctly errors. Self-type test suite 152/152. runtests.py self: 10
errors, matching an unmodified checkout exactly (pre-existing environment
gaps -- missing psutil/tomli/filelock/setuptools stubs), confirmed by
running the identical check against the unmodified base commit. Full
test suite (mypy/test + mypyc/test): 13627 passed, 410 skipped, 10
xfailed; the 12 failures and 3 collection errors present are pre-existing
environment gaps (missing attrs/mypyc-build deps), confirmed identical on
an unmodified checkout.

The code above and in this commit was written by Claude and reviewed by
Moussa Mikhail. The issue and the PR description accompanying it are his
own, written by him.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REqZNys4H8tiFU7Bb6Qj2B
Adds testTypingSelfIdentityTypeAlias (mirroring the existing
testTypingSelfAnnotatedType) plus a minimal regression case, covering:
self: Ident[Self], both orderings of the classmethod form
(cls: Ident[type[Self]] and cls: type[Ident[Self]]), and a negative case
confirming a non-Self alias argument (Ident[int]) still raises the real
self-type error.

Adds testTypingSelfIdentityTypeAliasSubstitution covering what the
substitution-based implementation handles beyond the literal "type X[T]
= T" shape, for free: a union that only collapses to Self once
substituted (type UnionSame[T] = T | T), an alias with an extra unused
type parameter in either position (type Extra[T, S] = T and type
Extra2[S, T] = T), the classmethod form of the same, and a negative case
confirming Self supplied for an alias's *unused* parameter is still
correctly rejected, not silently accepted.

Writing the classmethod cases in the first test is what surfaced the gap
the previous commit's classmethod-branch handling fixes -- cls:
Ident[type[Self]] was still being wrongly rejected until that commit
added the same handling to the classmethod branch, not just the plain
branch.

Separately, found and worked around an unrelated pre-existing mypy issue
while writing these tests: a generic type alias whose value is *exactly*
a bare type parameter (type Ident[T] = T) crashes the full checker
(AssertionError: Var(TypeVar), in checker.named_type) under the test
suite's minimal typing.pyi stub, where TypeVar is a dummy (`TypeVar = 0`)
rather than a real class -- checker.py's alias-value-expression check
calls named_type("typing.TypeVar") and asserts it resolves to a TypeInfo.
A wrapped alias (type Wrapped[T] = list[T]) does not trigger it, so it's
specific to the bare-identity shape. [typing fixtures/typing-full.pyi]
(which has a real TypeVar class) sidesteps it; the new cases declare it
for that reason. Not touched here -- it's unrelated to self-annotation
handling and worth its own report.

Verified: all new cases pass; self-type test suite 152/152 (was 149
before either commit). Full test suite (mypy/test + mypyc/test): 13627
passed, 410 skipped, 10 xfailed, with the same 12 failures / 3 collection
errors as before these changes, confirmed identical to an unmodified
checkout (pre-existing environment gaps: missing attrs/mypyc-build
deps).

The code above and in this commit was written by Claude and reviewed by
Moussa Mikhail. The issue and the PR description accompanying it are his
own, written by him.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REqZNys4H8tiFU7Bb6Qj2B
@Moussa-Mikhail
Moussa-Mikhail force-pushed the fix-self-in-identity-alias branch from 1ecaab8 to 5a326cd Compare October 12, 2026 01:33
@github-actions

Copy link
Copy Markdown
Contributor

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

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.

self: Ident[Self] wrongly rejected for an identity type alias (type Ident[T] = T).

1 participant