Skip to content

Keep the qualified name of a nested class in a stub - #1422

Merged
wjakob merged 1 commit into
wjakob:masterfrom
yosh-matsuda:fix-nested-class-name
Aug 24, 2026
Merged

Keep the qualified name of a nested class in a stub#1422
wjakob merged 1 commit into
wjakob:masterfrom
yosh-matsuda:fix-nested-class-name

Conversation

@yosh-matsuda

Copy link
Copy Markdown
Contributor

Inside a class body, stubgen strips the prefix of the enclosing class and writes the short name of a nested class (stubgen.py:844). The two type checkers do not agree on what that name means:

# m.pyi
class Sibling:
    top: int

class MyClass:
    class Sibling:
        nested: str
    def g(self, arg: Sibling, /) -> None: ...
MyClass().g(MyClass.Sibling())
> mypy       Success: no issues found
> pyright    error: Argument of type "Sibling" cannot be assigned to parameter "arg" of type "Sibling"
             "m.MyClass.Sibling" is not assignable to "m.Sibling"

mypy reads the short name in the scope of the class, and pyright reads it as the module level name. Nothing in the message points at the stub, and the type is silently wrong whenever the module holds a class of that name.

A qualified name has no such ambiguity, and both checkers accept it:

    def g(self, arg: MyClass.Sibling, /) -> None: ...

Relation to #1333

The rule came from #1333, which states that a type checker cannot resolve EnumWrapper.Value inside the body of EnumWrapper and reports Unknown. A stub is never executed, so neither checker has that problem today:

class EnumWrapper:
    class Value(enum.Enum):
        Alpha = 0
    Alpha: EnumWrapper.Value = EnumWrapper.Value.Alpha
    def get_value(self) -> EnumWrapper.Value: ...
> pyright    Type of "EnumWrapper.Alpha" is "Value"
> mypy       Revealed type is "q.EnumWrapper.Value"

The generated test_enum_ext.pyi gives the same result: pyright reports the identical set of 8 diagnostics with the short and the qualified name, and mypy the same 4. Both sets are unrelated to the names of the nested classes.

Inside a class body, stubgen stripped the prefix of the enclosing class
and wrote the short name of a nested class. The two type checkers do not
agree on such a name: mypy reads it in the scope of the class, while
pyright reads it as the module level name. A module that holds both
'Sibling' and 'MyClass.Sibling' therefore gets a wrong type from pyright,
with no message that points at the stub.

The name now stays qualified, which both checkers resolve to the nested
class. The test suite gains a module level class that collides with a
nested one.
@wjakob

wjakob commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Ugh, looks like you are right. At the time, I tried the offending example code from #1333 in PyRight, but I think I did so after saving it into a .py file and not a .pyi file.

Turns out both PyRight and MyPy are perfectly happy with the nested type names when they occur in a .pyi stub. So the premise of this change was incorrect.

@wjakob
wjakob merged commit 1042931 into wjakob:master Aug 24, 2026
39 checks passed
@markjm

markjm commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

apologies for the churn - fwiw this does look to be a problem for my case still, but is perhaps an issue/disparate behaviour specifically with ty - I should have checked across all the common checkers. My bad!

https://play.ty.dev/257e3e12-0593-41b4-8187-d7b7a85a374e

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.

3 participants