From 60ca82b1059c15db642d7aaa4eab90cf85c81f5f Mon Sep 17 00:00:00 2001 From: istoolsfox Date: Thu, 17 Sep 2026 18:16:07 +0800 Subject: [PATCH] Resolve bare Protocol and Generic references in type position Typeshed declares the Protocol and Generic special forms as X: type[_X], mirroring the runtime, where both are classes on 3.12+. Mypy already uses those declarations when the names appear in expression context, but a bare reference in type position still failed with 'Variable ... is not valid as a type', so e.g. 'x: type[Protocol] = Protocol' was rejected. Resolve a bare reference to Instance(_Protocol)/Instance(_Generic), matching what the declarations say the runtime objects are. If the private classes are not defined in the stubs (before the typeshed sync lands), keep the regular 'not valid as a type' error instead of resolving to Any, so behavior only changes together with the stubs. References with arguments (Protocol[T]) keep failing as before, and class bases are unaffected (handled separately in semanal). The expression-context reproducer from #21940 is already fixed by the typeshed declaration change itself; this handles the remaining type-position gap. Pyright currently rejects these examples too, so the divergence may be worth a look on the maintainer side. Refs #21940 --- mypy/typeanal.py | 24 +++++++++++++++++++ test-data/unit/check-generics.test | 8 +++++++ test-data/unit/check-protocols.test | 20 ++++++++++++++++ test-data/unit/fixtures/typing-async.pyi | 6 +++-- test-data/unit/fixtures/typing-full.pyi | 6 +++-- test-data/unit/fixtures/typing-medium.pyi | 6 +++-- test-data/unit/fixtures/typing-namedtuple.pyi | 3 ++- test-data/unit/fixtures/typing-override.pyi | 3 ++- .../unit/fixtures/typing-typeddict-iror.pyi | 6 +++-- test-data/unit/fixtures/typing-typeddict.pyi | 6 +++-- test-data/unit/lib-stub/typing.pyi | 6 +++-- test-data/unit/lib-stub/typing_extensions.pyi | 3 ++- 12 files changed, 82 insertions(+), 15 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 8d500c54364ae..bc0dbfc997dd6 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -67,6 +67,7 @@ LITERAL_TYPE_NAMES, MYPYC_NATIVE_INT_NAMES, NEVER_NAMES, + PROTOCOL_NAMES, TUPLE_NAMES, TYPE_ALIAS_NAMES, TYPE_NAMES, @@ -702,6 +703,29 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ self.fail(f'{type_str} can\'t contain "{bad_item_name}"', t, code=codes.VALID_TYPE) item = AnyType(TypeOfAny.from_error) return TypeType.make_normalized(item, line=t.line, column=t.column) + elif fullname in PROTOCOL_NAMES or fullname == "typing.Generic": + # A bare reference to Protocol or Generic in type expression position. + # Stubs declare both as X: type[_X] (mirroring the runtime, where they + # are classes on 3.12+), so e.g. 'type[Protocol]' must accept the + # special form itself. + # https://github.com/python/mypy/issues/21940 + if t.args or t.empty_tuple_index: + # 'Protocol[...]'/ 'Generic[...]' are class base syntax, not a type. + return None + private = "_Protocol" if fullname in PROTOCOL_NAMES else "_Generic" + module = "typing_extensions" if fullname.startswith("typing_extensions.") else "typing" + sym = self.api.lookup_fully_qualified_or_none(f"{module}.{private}") + if sym is not None and isinstance(sym.node, PlaceholderNode): + if self.api.is_incomplete_namespace(module): + # The private class is not defined yet, defer until it is. + self.api.record_incomplete_ref() + return AnyType(TypeOfAny.special_form) + return None + if sym is None or not isinstance(sym.node, TypeInfo): + # Stub sets that do not declare the private class yet: keep the + # regular 'not valid as a type' error instead of resolving to Any. + return None + return Instance(sym.node, [], line=t.line, column=t.column) elif fullname in ("typing_extensions.TypeForm", "typing.TypeForm"): if len(t.args) == 0: any_type = self.get_omitted_any(t) diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index b8f7a5699e199..0b67bd6dee0da 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -3737,3 +3737,11 @@ def test(tp: type[T]) -> T: ... class C(Generic[T]): ... reveal_type(test(C)) # N: Revealed type is "__main__.C[Any]" + +[case testGenericSpecialFormAsType] +from typing import Generic + +g: type[Generic] = Generic + +def f(t: type[Generic]) -> None: ... +f(Generic) diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 6e86507eb48d7..ac0f881fead64 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4809,3 +4809,23 @@ bad_rep(t) # E: Argument 1 to "bad_rep" has incompatible type "C"; expected "P[ # N: Got: \ # N: def rep(self) -> C [builtins fixtures/tuple.pyi] + +[case testProtocolSpecialFormAsType] +from typing import Protocol + +x: type[Protocol] = Protocol + +def f(t: type[Protocol]) -> None: ... +f(Protocol) + +ts: tuple[type[Protocol], ...] = (Protocol,) +[builtins fixtures/bool.pyi] + +[case testTypingExtensionsProtocolSpecialFormAsType] +from typing_extensions import Protocol + +x: type[Protocol] = Protocol + +def f(t: type[Protocol]) -> None: ... +f(Protocol) +[builtins fixtures/dict.pyi] diff --git a/test-data/unit/fixtures/typing-async.pyi b/test-data/unit/fixtures/typing-async.pyi index 3773572e6eefe..0409e3f37bedc 100644 --- a/test-data/unit/fixtures/typing-async.pyi +++ b/test-data/unit/fixtures/typing-async.pyi @@ -14,8 +14,10 @@ Any = object() Union = 0 Optional = 0 TypeVar = 0 -Generic = 0 -Protocol = 0 +class _Generic: ... +Generic: type[_Generic] +class _Protocol: ... +Protocol: type[_Protocol] Tuple = 0 Callable = 0 NamedTuple = 0 diff --git a/test-data/unit/fixtures/typing-full.pyi b/test-data/unit/fixtures/typing-full.pyi index 59e2f9de9929d..410a1f9547e57 100644 --- a/test-data/unit/fixtures/typing-full.pyi +++ b/test-data/unit/fixtures/typing-full.pyi @@ -25,8 +25,10 @@ def assert_type(o, t): ... overload = 0 Any = object() Optional = 0 -Generic = 0 -Protocol = 0 +class _Generic: ... +Generic: type[_Generic] +class _Protocol: ... +Protocol: type[_Protocol] Tuple = 0 _promote = 0 Type = 0 diff --git a/test-data/unit/fixtures/typing-medium.pyi b/test-data/unit/fixtures/typing-medium.pyi index 077d4eebf7d39..ab1abcdf95343 100644 --- a/test-data/unit/fixtures/typing-medium.pyi +++ b/test-data/unit/fixtures/typing-medium.pyi @@ -12,8 +12,10 @@ Any = object() Union = 0 Optional = 0 TypeVar = 0 -Generic = 0 -Protocol = 0 +class _Generic: ... +Generic: type[_Generic] +class _Protocol: ... +Protocol: type[_Protocol] Tuple = 0 Callable = 0 _promote = 0 diff --git a/test-data/unit/fixtures/typing-namedtuple.pyi b/test-data/unit/fixtures/typing-namedtuple.pyi index 5b0ef0845dadf..348711872c5cc 100644 --- a/test-data/unit/fixtures/typing-namedtuple.pyi +++ b/test-data/unit/fixtures/typing-namedtuple.pyi @@ -1,5 +1,6 @@ TypeVar = 0 -Generic = 0 +class _Generic: ... +Generic: type[_Generic] Any = object() overload = 0 Type = 0 diff --git a/test-data/unit/fixtures/typing-override.pyi b/test-data/unit/fixtures/typing-override.pyi index a0287524c84a8..ce86d49c10814 100644 --- a/test-data/unit/fixtures/typing-override.pyi +++ b/test-data/unit/fixtures/typing-override.pyi @@ -1,5 +1,6 @@ TypeVar = 0 -Generic = 0 +class _Generic: ... +Generic: type[_Generic] Any = object() overload = 0 Type = 0 diff --git a/test-data/unit/fixtures/typing-typeddict-iror.pyi b/test-data/unit/fixtures/typing-typeddict-iror.pyi index 845ac6cf208fd..6e45730400615 100644 --- a/test-data/unit/fixtures/typing-typeddict-iror.pyi +++ b/test-data/unit/fixtures/typing-typeddict-iror.pyi @@ -16,8 +16,10 @@ Any = object() Union = 0 Optional = 0 TypeVar = 0 -Generic = 0 -Protocol = 0 +class _Generic: ... +Generic: type[_Generic] +class _Protocol: ... +Protocol: type[_Protocol] Tuple = 0 Callable = 0 NamedTuple = 0 diff --git a/test-data/unit/fixtures/typing-typeddict.pyi b/test-data/unit/fixtures/typing-typeddict.pyi index 0bc5637b32708..057a3b8511859 100644 --- a/test-data/unit/fixtures/typing-typeddict.pyi +++ b/test-data/unit/fixtures/typing-typeddict.pyi @@ -15,8 +15,10 @@ Any = object() Union = 0 Optional = 0 TypeVar = 0 -Generic = 0 -Protocol = 0 +class _Generic: ... +Generic: type[_Generic] +class _Protocol: ... +Protocol: type[_Protocol] Tuple = 0 Callable = 0 NamedTuple = 0 diff --git a/test-data/unit/lib-stub/typing.pyi b/test-data/unit/lib-stub/typing.pyi index 00fce56920b75..fcd2193f9646d 100644 --- a/test-data/unit/lib-stub/typing.pyi +++ b/test-data/unit/lib-stub/typing.pyi @@ -15,8 +15,10 @@ Any = object() Union = 0 Optional = 0 TypeVar = 0 -Generic = 0 -Protocol = 0 +class _Generic: ... +Generic: type[_Generic] +class _Protocol: ... +Protocol: type[_Protocol] Tuple = 0 Callable = 0 NamedTuple = 0 diff --git a/test-data/unit/lib-stub/typing_extensions.pyi b/test-data/unit/lib-stub/typing_extensions.pyi index 47bae94a7f7d7..91603d84e0c92 100644 --- a/test-data/unit/lib-stub/typing_extensions.pyi +++ b/test-data/unit/lib-stub/typing_extensions.pyi @@ -18,7 +18,8 @@ class _SpecialForm: pass NamedTuple = 0 -Protocol: _SpecialForm = ... +class _Protocol: ... +Protocol: type[_Protocol] def runtime_checkable(x: _T) -> _T: pass runtime = runtime_checkable