Skip to content

Handle corner case: protocol vs classvar vs descriptor #19277

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/source/protocols.rst
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,53 @@ the parameters are positional-only. Example (using the legacy syntax for generic
copy_a = copy_b # OK
copy_b = copy_a # Also OK

Binding of types in protocol attributes
***************************************

All protocol attributes annotations are treated as externally visible types
of those attributes. This means that for example callables are not bound,
and descriptors are not invoked:

.. code-block:: python

from typing import Callable, Protocol, overload

class Integer:
@overload
def __get__(self, instance: None, owner: object) -> Integer: ...
@overload
def __get__(self, instance: object, owner: object) -> int: ...
# <some implementation>

class Example(Protocol):
foo: Callable[[object], int]
bar: Integer

ex: Example
reveal_type(ex.foo) # Revealed type is Callable[[object], int]
reveal_type(ex.bar) # Revealed type is Integer

In other words, protocol attribute types are handled as they would appear in a
``self`` attribute annotation in a regular class. If you want some protocol
attributes to be handled as though they were defined at class level, you should
declare them explicitly using ``ClassVar[...]``. Continuing previous example:

.. code-block:: python

from typing import ClassVar

class OtherExample(Protocol):
# This style is *not recommended*, but may be needed to reuse
# some complex callable types. Otherwise use regular methods.
foo: ClassVar[Callable[[object], int]]
# This may be needed to mimic descriptor access on Type[...] types,
# otherwise use a plain "bar: int" style.
bar: ClassVar[Integer]

ex2: OtherExample
reveal_type(ex2.foo) # Revealed type is Callable[[], int]
reveal_type(ex2.bar) # Revealed type is int

.. _predefined_protocols_reference:

Predefined protocol reference
Expand Down
12 changes: 11 additions & 1 deletion mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1457,14 +1457,24 @@ def get_member_flags(name: str, itype: Instance, class_obj: bool = False) -> set
flags = {IS_VAR}
if not v.is_final:
flags.add(IS_SETTABLE)
if v.is_classvar:
# TODO: define cleaner rules for class vs instance variables.
if v.is_classvar and not is_descriptor(v.type):
flags.add(IS_CLASSVAR)
if class_obj and v.is_inferred:
flags.add(IS_CLASSVAR)
return flags
return set()


def is_descriptor(typ: Type | None) -> bool:
typ = get_proper_type(typ)
if isinstance(typ, Instance):
return typ.type.get("__get__") is not None
if isinstance(typ, UnionType):
return all(is_descriptor(item) for item in typ.relevant_items())
return False


def find_node_type(
node: Var | FuncBase,
itype: Instance,
Expand Down
44 changes: 44 additions & 0 deletions test-data/unit/check-protocols.test
Original file line number Diff line number Diff line change
Expand Up @@ -4602,3 +4602,47 @@ def deco(fn: Callable[[], T]) -> Callable[[], list[T]]: ...
@deco
def defer() -> int: ...
[builtins fixtures/list.pyi]

[case testProtocolClassValDescriptor]
from typing import Any, Protocol, overload, ClassVar, Type

class Desc:
@overload
def __get__(self, instance: None, owner: object) -> Desc: ...
@overload
def __get__(self, instance: object, owner: object) -> int: ...
def __get__(self, instance, owner):
pass

class P(Protocol):
x: ClassVar[Desc]

class C:
x = Desc()

t: P = C()
reveal_type(t.x) # N: Revealed type is "builtins.int"
tt: Type[P] = C
reveal_type(tt.x) # N: Revealed type is "__main__.Desc"

bad: P = C # E: Incompatible types in assignment (expression has type "type[C]", variable has type "P") \
# N: Following member(s) of "C" have conflicts: \
# N: x: expected "int", got "Desc"

[case testProtocolClassValCallable]
from typing import Any, Protocol, overload, ClassVar, Type, Callable

class P(Protocol):
foo: Callable[[object], int]
bar: ClassVar[Callable[[object], int]]

class C:
foo: Callable[[object], int]
bar: ClassVar[Callable[[object], int]]

t: P = C()
reveal_type(t.foo) # N: Revealed type is "def (builtins.object) -> builtins.int"
reveal_type(t.bar) # N: Revealed type is "def () -> builtins.int"
tt: Type[P] = C
reveal_type(tt.foo) # N: Revealed type is "def (builtins.object) -> builtins.int"
reveal_type(tt.bar) # N: Revealed type is "def (builtins.object) -> builtins.int"