You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
typing.Protocol is explicitly treated as not a type #21940
I traced this one — the value-position behaviour doesn't come from mypy itself but from the typeshed declaration, so the fix belongs there.
Diagnosis. For a bare Protocol reference in value position, reveal_type(Protocol) gives typing._SpecialForm. That comes straight from typeshed's stdlib/typing.pyi, which declares Protocol: _SpecialForm (line 241). mypy just consumes that declaration: _SpecialForm is an object subclass, not a type, hence the comparison-overlap and arg-type errors against tuple[type, ...].
Base-class positions are unaffected because mypy special-cases Protocol there by fullname (PROTOCOL_NAMES in mypy/types.py, used in mypy/semanal.py), which is why class A(Protocol) works fine today.
Why Protocol is a genuine outlier. Every other _SpecialForm declaration (Any, ClassVar, Final, Literal, ...) refers to something that is not a class at runtime. Protocol is the odd one out: isinstance(Protocol, type) is True, it has __name__/__bases__ ((typing.Generic,)), and runtime APIs hand it out as a class. Pyright already models it as a class in value position.
Where this should be fixed. Modeling Protocol as a real class in typeshed's typing.pyi (while keeping checker special-casing for base-class and subscript positions) is the direction that matches runtime behavior, but it's a core-stub change that affects every type checker consuming typeshed, so it needs a typeshed discussion before anyone touches the stub.
So I'd suggest either closing this in favor of a typeshed discussion, or leaving it open as a tracking issue for that change — happy to open the typeshed discussion if that's preferred.
I don't know why I assumed the types were being taken from typing.py, I guess it makes sense mypy is using the typeshed. I am perfectly fine with someone moving this to typeshed for resolution.
Now, one of the mypy internals you mentioned raises some further questions I would like to explore. Namely, special-casing Protocol with hardcoded PROTOCOL_NAMES. For context, I am designing my own custom Protocol. Internally, it is an exact replica of Protocol, metaclass and all, with all expected behaviors present (it looks like a Protocol and quacks like a Protocol). Its purpose is to extend the Protocol interface, adding optional rich runtime comparison checks, while still being a drop-in replacement for the internal implementation. It works exactly as intended, including decorating a new protocol with @typing.runtime_checkable, except for when type checking with mypy:
iftyping.TYPE_CHECKING:
RuntimeProtocol=typing.Protocolelse:
classRuntimeProtocol(typing.Generic, metaclass=_RuntimeProtocolMeta):
... # implementation here@typing.runtime_checkableclassNewProto(RuntimeProtocol): # Variable "RuntimeProtocol" is not valid as a type # @runtime_checkable can only be used with protocol classes
My first issue, valid-type error, will be fixed when Protocol is correctly identified as a class. It seems that the second misc error for using @runtime_checkable would persist if valid Protocol names are hardcoded. Perhaps a discussion could be had on whether or not hardcoding valid protocol names would still be necessary? I am not sure what the mypy stance is on deviations from other type checkers, so while I want this to work as part of my public API, I understand it may not be important to your project. My argument is simply: runtime_checkable does not require Protocol, it simply does an internal duck-type check for a "protocol-like".
Thanks for the follow-up — I spent some time digging into the hardcoded-names question.
My reading is that the hardcoding itself is unlikely to go away: mypy identifies every typing feature (Final, TypeAlias, overload, ...) by fully-qualified name, and as a static tool it has no runtime metaclass information to duck-type "is this a protocol", so it has to anchor on the typeshed declarations.
Experimenting with your pattern surfaced what I think is the real gap, and it is narrower than removing the names: is_protocol does not propagate through protocol base classes, so @runtime_checkable rejects even ordinary protocol subclasses, inconsistent with Pyright and with the runtime duck-typed check. I've opened #21950 with a minimal repro and details — your case reduces to exactly that once the valid-type part is out of the way.
For your snippet specifically, one immediate improvement: define RuntimeProtocol as a class (not an assignment) in the TYPE_CHECKING branch — that clears the valid-type/Invalid base class errors regardless of the decorator issue.
To be clear, these are just my personal observations and my reasoning may be off. I'd suggest opening the typeshed part there as discussed, and following #21950 for the propagation question so the mypy team can weigh in on the design.
Protocol is special cased, like all typing special forms. It not being assignable to type is a bug in subtyping. We probably don't allow type aliases to typing.Protocol since it's kinda silly to, but that could be a feature. (you should be able to work around it with from typing import Protocol as RuntimeProtocol)
@istoolsfox
The issue you raised makes no sense, and your example should fail type checking, as it fails with TypeError when runtime_checkable decorator evaluates the new class. This is expected behavior according to the documented Protocol API.
@A5rocks
My example was a naive approach on purpose, used to illustrate my point, and is a pattern I try to avoid at all cost. FWIW, the type alias technically did "work", as seen by the fact that it referenced the "non-type" of Protocol. My issue boils down to: does Protocol need to be special-cased in typeshed, since the reasons behind using _SpecialCase do not apply to it? All of the other special-case classes cannot be instantiated or subclassed, while Protocol can be instantiated and was explicitly created to be subclassed. It necessarily is a type by design, and reporting the original errors I posted is definitely unexpected behavior.
As for runtime_checkable reporting an error, I understand that removing the hardcoded names probably creates a lot of new work internally, so I won't push any further on that. While I would like for my API to allow typing.runtime_checkable to decorate my protocols without type errors, I am willing to compromise with the type system and create my own decorator where I can privately ignore type errors for the end users.
You're right, and thanks for the correction. I verified on 3.12.13, 3.13.14 and 3.14.6: all three raise TypeError: @runtime_checkable can be only applied to protocol classes for that example. So the "runtime accepts it" claim in #21950 was wrong, and mypy's rejection of that example matches the runtime. Nothing left to fix on the mypy side there.
Update: I tested the same approach typeshed used for Generic — python/typeshed#14583 declares it as type[_Generic] and its description names Protocol as a future candidate for the same treatment. With Protocol: type[_Protocol] in typing.pyi and typing_extensions.pyi, the repro here checks clean, and mypy's protocol, typing and fine-grained suites pass (513 tests). Opened python/typeshed#16386
One correction to my earlier comment: I said the fix "belongs in typeshed" — A5rocks is right that this is a subtyping bug. The declaration change is what lets subtyping see Protocol as the class it already is at runtime, same mechanism as Generic.
Activity
istoolsfox commentedon Sep 6, 2026
I traced this one — the value-position behaviour doesn't come from mypy itself but from the typeshed declaration, so the fix belongs there.
Diagnosis. For a bare
Protocolreference in value position,reveal_type(Protocol)givestyping._SpecialForm. That comes straight from typeshed'sstdlib/typing.pyi, which declaresProtocol: _SpecialForm(line 241). mypy just consumes that declaration:_SpecialFormis an object subclass, not atype, hence thecomparison-overlapandarg-typeerrors againsttuple[type, ...].Base-class positions are unaffected because mypy special-cases
Protocolthere by fullname (PROTOCOL_NAMESinmypy/types.py, used inmypy/semanal.py), which is whyclass A(Protocol)works fine today.Why Protocol is a genuine outlier. Every other
_SpecialFormdeclaration (Any,ClassVar,Final,Literal, ...) refers to something that is not a class at runtime.Protocolis the odd one out:isinstance(Protocol, type)isTrue, it has__name__/__bases__((typing.Generic,)), and runtime APIs hand it out as a class. Pyright already models it as a class in value position.Where this should be fixed. Modeling
Protocolas a real class in typeshed'styping.pyi(while keeping checker special-casing for base-class and subscript positions) is the direction that matches runtime behavior, but it's a core-stub change that affects every type checker consuming typeshed, so it needs a typeshed discussion before anyone touches the stub.So I'd suggest either closing this in favor of a typeshed discussion, or leaving it open as a tracking issue for that change — happy to open the typeshed discussion if that's preferred.
chet-manley commentedon Sep 6, 2026
I don't know why I assumed the types were being taken from
typing.py, I guess it makes sense mypy is using the typeshed. I am perfectly fine with someone moving this to typeshed for resolution.Now, one of the mypy internals you mentioned raises some further questions I would like to explore. Namely, special-casing Protocol with hardcoded
PROTOCOL_NAMES. For context, I am designing my own custom Protocol. Internally, it is an exact replica of Protocol, metaclass and all, with all expected behaviors present (it looks like a Protocol and quacks like a Protocol). Its purpose is to extend the Protocol interface, adding optional rich runtime comparison checks, while still being a drop-in replacement for the internal implementation. It works exactly as intended, including decorating a new protocol with@typing.runtime_checkable, except for when type checking with mypy:My first issue, valid-type error, will be fixed when Protocol is correctly identified as a class. It seems that the second misc error for using
@runtime_checkablewould persist if valid Protocol names are hardcoded. Perhaps a discussion could be had on whether or not hardcoding valid protocol names would still be necessary? I am not sure what the mypy stance is on deviations from other type checkers, so while I want this to work as part of my public API, I understand it may not be important to your project. My argument is simply:runtime_checkabledoes not requireProtocol, it simply does an internal duck-type check for a "protocol-like".istoolsfox commentedon Sep 7, 2026
Thanks for the follow-up — I spent some time digging into the hardcoded-names question.
My reading is that the hardcoding itself is unlikely to go away: mypy identifies every typing feature (
Final,TypeAlias,overload, ...) by fully-qualified name, and as a static tool it has no runtime metaclass information to duck-type "is this a protocol", so it has to anchor on the typeshed declarations.Experimenting with your pattern surfaced what I think is the real gap, and it is narrower than removing the names:
is_protocoldoes not propagate through protocol base classes, so@runtime_checkablerejects even ordinary protocol subclasses, inconsistent with Pyright and with the runtime duck-typed check. I've opened #21950 with a minimal repro and details — your case reduces to exactly that once thevalid-typepart is out of the way.For your snippet specifically, one immediate improvement: define
RuntimeProtocolas a class (not an assignment) in theTYPE_CHECKINGbranch — that clears thevalid-type/Invalid base classerrors regardless of the decorator issue.To be clear, these are just my personal observations and my reasoning may be off. I'd suggest opening the typeshed part there as discussed, and following #21950 for the propagation question so the mypy team can weigh in on the design.
A5rocks commentedon Sep 7, 2026
Oh my god please don't trust claudeslop.
Protocol is special cased, like all typing special forms. It not being assignable to
typeis a bug in subtyping. We probably don't allow type aliases totyping.Protocolsince it's kinda silly to, but that could be a feature. (you should be able to work around it withfrom typing import Protocol as RuntimeProtocol)chet-manley commentedon Sep 7, 2026
@istoolsfox
The issue you raised makes no sense, and your example should fail type checking, as it fails with
TypeErrorwhenruntime_checkabledecorator evaluates the new class. This is expected behavior according to the documented Protocol API.@A5rocks
My example was a naive approach on purpose, used to illustrate my point, and is a pattern I try to avoid at all cost. FWIW, the type alias technically did "work", as seen by the fact that it referenced the "non-type" of Protocol. My issue boils down to: does Protocol need to be special-cased in typeshed, since the reasons behind using
_SpecialCasedo not apply to it? All of the other special-case classes cannot be instantiated or subclassed, while Protocol can be instantiated and was explicitly created to be subclassed. It necessarily is atypeby design, and reporting the original errors I posted is definitely unexpected behavior.As for
runtime_checkablereporting an error, I understand that removing the hardcoded names probably creates a lot of new work internally, so I won't push any further on that. While I would like for my API to allowtyping.runtime_checkableto decorate my protocols without type errors, I am willing to compromise with the type system and create my own decorator where I can privately ignore type errors for the end users.istoolsfox commentedon Sep 8, 2026
You're right, and thanks for the correction. I verified on 3.12.13, 3.13.14 and 3.14.6: all three raise
TypeError: @runtime_checkable can be only applied to protocol classesfor that example. So the "runtime accepts it" claim in #21950 was wrong, and mypy's rejection of that example matches the runtime. Nothing left to fix on the mypy side there.istoolsfox commentedon Sep 11, 2026
Update: I tested the same approach typeshed used for
Generic— python/typeshed#14583 declares it astype[_Generic]and its description namesProtocolas a future candidate for the same treatment. WithProtocol: type[_Protocol]intyping.pyiandtyping_extensions.pyi, the repro here checks clean, and mypy's protocol, typing and fine-grained suites pass (513 tests). Opened python/typeshed#16386One correction to my earlier comment: I said the fix "belongs in typeshed" — A5rocks is right that this is a subtyping bug. The declaration change is what lets subtyping see
Protocolas the class it already is at runtime, same mechanism asGeneric.Accept Protocol and Generic in type expression position
Accept Protocol and Generic in type expression position
Resolve bare Protocol and Generic references in type position