mirror of
				https://github.com/astral-sh/ruff.git
				synced 2025-10-31 12:05:57 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			2311 lines
		
	
	
	
		
			72 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			2311 lines
		
	
	
	
		
			72 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # Protocols
 | |
| 
 | |
| > [!NOTE]
 | |
| >
 | |
| > See also:
 | |
| >
 | |
| > - The [typing specification section on protocols][typing_spec_protocols]
 | |
| > - The many [protocol conformance tests] provided by the Typing Council for type checkers
 | |
| > - Mypy's [documentation][mypy_protocol_docs] and [tests][mypy_protocol_tests] for protocols
 | |
| 
 | |
| Most types in Python are *nominal* types: a fully static nominal type `X` is only a subtype of
 | |
| another fully static nominal type `Y` if the class `X` is a subclass of the class `Y`.
 | |
| `typing.Protocol` (or its backport, `typing_extensions.Protocol`) can be used to define *structural*
 | |
| types, on the other hand: a type which is defined by its properties and behavior.
 | |
| 
 | |
| ## Defining a protocol
 | |
| 
 | |
| ```toml
 | |
| [environment]
 | |
| python-version = "3.12"
 | |
| ```
 | |
| 
 | |
| A protocol is defined by inheriting from the `Protocol` class, which is annotated as an instance of
 | |
| `_SpecialForm` in typeshed's stubs.
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol
 | |
| 
 | |
| class MyProtocol(Protocol): ...
 | |
| 
 | |
| reveal_type(MyProtocol.__mro__)  # revealed: tuple[<class 'MyProtocol'>, typing.Protocol, typing.Generic, <class 'object'>]
 | |
| ```
 | |
| 
 | |
| Just like for any other class base, it is an error for `Protocol` to appear multiple times in a
 | |
| class's bases:
 | |
| 
 | |
| ```py
 | |
| class Foo(Protocol, Protocol): ...  # error: [duplicate-base]
 | |
| 
 | |
| reveal_type(Foo.__mro__)  # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
 | |
| ```
 | |
| 
 | |
| Protocols can also be generic, either by including `Generic[]` in the bases list, subscripting
 | |
| `Protocol` directly in the bases list, using PEP-695 type parameters, or some combination of the
 | |
| above:
 | |
| 
 | |
| ```py
 | |
| from typing import TypeVar, Generic
 | |
| 
 | |
| T = TypeVar("T")
 | |
| 
 | |
| class Bar0(Protocol[T]):
 | |
|     x: T
 | |
| 
 | |
| class Bar1(Protocol[T], Generic[T]):
 | |
|     x: T
 | |
| 
 | |
| class Bar2[T](Protocol):
 | |
|     x: T
 | |
| 
 | |
| # error: [invalid-generic-class] "Cannot both inherit from subscripted `Protocol` and use PEP 695 type variables"
 | |
| class Bar3[T](Protocol[T]):
 | |
|     x: T
 | |
| 
 | |
| # Note that this class definition *will* actually succeed at runtime,
 | |
| # unlike classes that combine PEP-695 type parameters with inheritance from `Generic[]`
 | |
| reveal_type(Bar3.__mro__)  # revealed: tuple[<class 'Bar3[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
 | |
| ```
 | |
| 
 | |
| It's an error to include both bare `Protocol` and subscripted `Protocol[]` in the bases list
 | |
| simultaneously:
 | |
| 
 | |
| ```py
 | |
| class DuplicateBases(Protocol, Protocol[T]):  # error: [duplicate-base]
 | |
|     x: T
 | |
| 
 | |
| # revealed: tuple[<class 'DuplicateBases[Unknown]'>, Unknown, <class 'object'>]
 | |
| reveal_type(DuplicateBases.__mro__)
 | |
| ```
 | |
| 
 | |
| The introspection helper `typing(_extensions).is_protocol` can be used to verify whether a class is
 | |
| a protocol class or not:
 | |
| 
 | |
| ```py
 | |
| from typing_extensions import is_protocol
 | |
| 
 | |
| reveal_type(is_protocol(MyProtocol))  # revealed: Literal[True]
 | |
| reveal_type(is_protocol(Bar0))  # revealed: Literal[True]
 | |
| reveal_type(is_protocol(Bar1))  # revealed: Literal[True]
 | |
| reveal_type(is_protocol(Bar2))  # revealed: Literal[True]
 | |
| reveal_type(is_protocol(Bar3))  # revealed: Literal[True]
 | |
| 
 | |
| class NotAProtocol: ...
 | |
| 
 | |
| reveal_type(is_protocol(NotAProtocol))  # revealed: Literal[False]
 | |
| ```
 | |
| 
 | |
| A type checker should follow the typeshed stubs if a non-class is passed in, and typeshed's stubs
 | |
| indicate that the argument passed in must be an instance of `type`.
 | |
| 
 | |
| ```py
 | |
| # We could also reasonably infer `Literal[False]` here, but it probably doesn't matter that much:
 | |
| # error: [invalid-argument-type]
 | |
| reveal_type(is_protocol("not a class"))  # revealed: bool
 | |
| ```
 | |
| 
 | |
| For a class to be considered a protocol class, it must have `Protocol` directly in its bases tuple:
 | |
| it is not sufficient for it to have `Protocol` in its MRO.
 | |
| 
 | |
| ```py
 | |
| class SubclassOfMyProtocol(MyProtocol): ...
 | |
| 
 | |
| # revealed: tuple[<class 'SubclassOfMyProtocol'>, <class 'MyProtocol'>, typing.Protocol, typing.Generic, <class 'object'>]
 | |
| reveal_type(SubclassOfMyProtocol.__mro__)
 | |
| 
 | |
| reveal_type(is_protocol(SubclassOfMyProtocol))  # revealed: Literal[False]
 | |
| ```
 | |
| 
 | |
| A protocol class may inherit from other protocols, however, as long as it re-inherits from
 | |
| `Protocol`:
 | |
| 
 | |
| ```py
 | |
| class SubProtocol(MyProtocol, Protocol): ...
 | |
| 
 | |
| reveal_type(is_protocol(SubProtocol))  # revealed: Literal[True]
 | |
| 
 | |
| class OtherProtocol(Protocol):
 | |
|     some_attribute: str
 | |
| 
 | |
| class ComplexInheritance(SubProtocol, OtherProtocol, Protocol): ...
 | |
| 
 | |
| # revealed: tuple[<class 'ComplexInheritance'>, <class 'SubProtocol'>, <class 'MyProtocol'>, <class 'OtherProtocol'>, typing.Protocol, typing.Generic, <class 'object'>]
 | |
| reveal_type(ComplexInheritance.__mro__)
 | |
| 
 | |
| reveal_type(is_protocol(ComplexInheritance))  # revealed: Literal[True]
 | |
| ```
 | |
| 
 | |
| If `Protocol` is present in the bases tuple, all other bases in the tuple must be protocol classes,
 | |
| or `TypeError` is raised at runtime when the class is created.
 | |
| 
 | |
| ```py
 | |
| # error: [invalid-protocol] "Protocol class `Invalid` cannot inherit from non-protocol class `NotAProtocol`"
 | |
| class Invalid(NotAProtocol, Protocol): ...
 | |
| 
 | |
| # revealed: tuple[<class 'Invalid'>, <class 'NotAProtocol'>, typing.Protocol, typing.Generic, <class 'object'>]
 | |
| reveal_type(Invalid.__mro__)
 | |
| 
 | |
| # error: [invalid-protocol] "Protocol class `AlsoInvalid` cannot inherit from non-protocol class `NotAProtocol`"
 | |
| class AlsoInvalid(MyProtocol, OtherProtocol, NotAProtocol, Protocol): ...
 | |
| 
 | |
| # revealed: tuple[<class 'AlsoInvalid'>, <class 'MyProtocol'>, <class 'OtherProtocol'>, <class 'NotAProtocol'>, typing.Protocol, typing.Generic, <class 'object'>]
 | |
| reveal_type(AlsoInvalid.__mro__)
 | |
| 
 | |
| class NotAGenericProtocol[T]: ...
 | |
| 
 | |
| # error: [invalid-protocol] "Protocol class `StillInvalid` cannot inherit from non-protocol class `NotAGenericProtocol`"
 | |
| class StillInvalid(NotAGenericProtocol[int], Protocol): ...
 | |
| 
 | |
| # revealed: tuple[<class 'StillInvalid'>, <class 'NotAGenericProtocol[int]'>, typing.Protocol, typing.Generic, <class 'object'>]
 | |
| reveal_type(StillInvalid.__mro__)
 | |
| ```
 | |
| 
 | |
| But two exceptions to this rule are `object` and `Generic`:
 | |
| 
 | |
| ```py
 | |
| from typing import TypeVar, Generic
 | |
| 
 | |
| T = TypeVar("T")
 | |
| 
 | |
| # Note: pyright and pyrefly do not consider this to be a valid `Protocol` class,
 | |
| # but mypy does (and has an explicit test for this behavior). Mypy was the
 | |
| # reference implementation for PEP-544, and its behavior also matches the CPython
 | |
| # runtime, so we choose to follow its behavior here rather than that of the other
 | |
| # type checkers.
 | |
| class Fine(Protocol, object): ...
 | |
| 
 | |
| reveal_type(Fine.__mro__)  # revealed: tuple[<class 'Fine'>, typing.Protocol, typing.Generic, <class 'object'>]
 | |
| 
 | |
| class StillFine(Protocol, Generic[T], object): ...
 | |
| class EvenThis[T](Protocol, object): ...
 | |
| class OrThis(Protocol[T], Generic[T]): ...
 | |
| class AndThis(Protocol[T], Generic[T], object): ...
 | |
| ```
 | |
| 
 | |
| And multiple inheritance from a mix of protocol and non-protocol classes is fine as long as
 | |
| `Protocol` itself is not in the bases list:
 | |
| 
 | |
| ```py
 | |
| class FineAndDandy(MyProtocol, OtherProtocol, NotAProtocol): ...
 | |
| 
 | |
| # revealed: tuple[<class 'FineAndDandy'>, <class 'MyProtocol'>, <class 'OtherProtocol'>, typing.Protocol, typing.Generic, <class 'NotAProtocol'>, <class 'object'>]
 | |
| reveal_type(FineAndDandy.__mro__)
 | |
| ```
 | |
| 
 | |
| But if `Protocol` is not present in the bases list, the resulting class doesn't count as a protocol
 | |
| class anymore:
 | |
| 
 | |
| ```py
 | |
| reveal_type(is_protocol(FineAndDandy))  # revealed: Literal[False]
 | |
| ```
 | |
| 
 | |
| A class does not *have* to inherit from a protocol class in order for it to be considered a subtype
 | |
| of that protocol (more on that below). However, classes that explicitly inherit from a protocol
 | |
| class are understood as subtypes of that protocol, the same as with nominal types:
 | |
| 
 | |
| ```py
 | |
| from ty_extensions import static_assert, is_subtype_of, is_assignable_to
 | |
| 
 | |
| static_assert(is_subtype_of(SubclassOfMyProtocol, MyProtocol))
 | |
| static_assert(is_assignable_to(SubclassOfMyProtocol, MyProtocol))
 | |
| 
 | |
| static_assert(is_subtype_of(SubProtocol, MyProtocol))
 | |
| static_assert(is_assignable_to(SubProtocol, MyProtocol))
 | |
| 
 | |
| static_assert(is_subtype_of(ComplexInheritance, SubProtocol))
 | |
| static_assert(is_assignable_to(ComplexInheritance, SubProtocol))
 | |
| 
 | |
| static_assert(is_subtype_of(ComplexInheritance, OtherProtocol))
 | |
| static_assert(is_assignable_to(ComplexInheritance, SubProtocol))
 | |
| 
 | |
| static_assert(is_subtype_of(FineAndDandy, MyProtocol))
 | |
| static_assert(is_assignable_to(FineAndDandy, MyProtocol))
 | |
| 
 | |
| static_assert(is_subtype_of(FineAndDandy, OtherProtocol))
 | |
| static_assert(is_assignable_to(FineAndDandy, OtherProtocol))
 | |
| ```
 | |
| 
 | |
| Note, however, that `Protocol` itself is not a type, so it is an error to pass it to `is_subtype_of`
 | |
| or `is_assignable_to`:
 | |
| 
 | |
| ```py
 | |
| is_subtype_of(MyProtocol, Protocol)  # error: [invalid-type-form]
 | |
| is_assignable_to(MyProtocol, Protocol)  # error: [invalid-type-form]
 | |
| ```
 | |
| 
 | |
| And it is also an error to use `Protocol` in type expressions:
 | |
| 
 | |
| ```py
 | |
| # fmt: off
 | |
| 
 | |
| def f(
 | |
|     x: Protocol,  # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions"
 | |
|     y: type[Protocol],  # TODO: should emit `[invalid-type-form]` here too
 | |
| ):
 | |
|     reveal_type(x)  # revealed: Unknown
 | |
| 
 | |
|     # TODO: should be `type[Unknown]`
 | |
|     reveal_type(y)  # revealed: @Todo(unsupported type[X] special form)
 | |
| 
 | |
| # fmt: on
 | |
| ```
 | |
| 
 | |
| Nonetheless, `Protocol` can still be used as the second argument to `issubclass()` at runtime:
 | |
| 
 | |
| ```py
 | |
| # Could also be `Literal[True]`, but `bool` is fine:
 | |
| reveal_type(issubclass(MyProtocol, Protocol))  # revealed: bool
 | |
| ```
 | |
| 
 | |
| ## `typing.Protocol` versus `typing_extensions.Protocol`
 | |
| 
 | |
| `typing.Protocol` and its backport in `typing_extensions` should be treated as exactly equivalent.
 | |
| 
 | |
| ```py
 | |
| import typing
 | |
| import typing_extensions
 | |
| from ty_extensions import static_assert, is_equivalent_to, TypeOf
 | |
| 
 | |
| static_assert(is_equivalent_to(TypeOf[typing.Protocol], TypeOf[typing_extensions.Protocol]))
 | |
| static_assert(is_equivalent_to(int | str | TypeOf[typing.Protocol], TypeOf[typing_extensions.Protocol] | str | int))
 | |
| 
 | |
| class Foo(typing.Protocol):
 | |
|     x: int
 | |
| 
 | |
| class Bar(typing_extensions.Protocol):
 | |
|     x: int
 | |
| 
 | |
| static_assert(typing_extensions.is_protocol(Foo))
 | |
| static_assert(typing_extensions.is_protocol(Bar))
 | |
| static_assert(is_equivalent_to(Foo, Bar))
 | |
| ```
 | |
| 
 | |
| The same goes for `typing.runtime_checkable` and `typing_extensions.runtime_checkable`:
 | |
| 
 | |
| ```py
 | |
| @typing_extensions.runtime_checkable
 | |
| class RuntimeCheckableFoo(typing.Protocol):
 | |
|     x: int
 | |
| 
 | |
| @typing.runtime_checkable
 | |
| class RuntimeCheckableBar(typing_extensions.Protocol):
 | |
|     x: int
 | |
| 
 | |
| static_assert(typing_extensions.is_protocol(RuntimeCheckableFoo))
 | |
| static_assert(typing_extensions.is_protocol(RuntimeCheckableBar))
 | |
| static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar))
 | |
| 
 | |
| # These should not error because the protocols are decorated with `@runtime_checkable`
 | |
| isinstance(object(), RuntimeCheckableFoo)
 | |
| isinstance(object(), RuntimeCheckableBar)
 | |
| ```
 | |
| 
 | |
| However, we understand that they are not necessarily the same symbol at the same memory address at
 | |
| runtime -- these reveal `bool` rather than `Literal[True]` or `Literal[False]`, which would be
 | |
| incorrect:
 | |
| 
 | |
| ```py
 | |
| reveal_type(typing.Protocol is typing_extensions.Protocol)  # revealed: bool
 | |
| reveal_type(typing.Protocol is not typing_extensions.Protocol)  # revealed: bool
 | |
| ```
 | |
| 
 | |
| ## Calls to protocol classes
 | |
| 
 | |
| <!-- snapshot-diagnostics -->
 | |
| 
 | |
| Neither `Protocol`, nor any protocol class, can be directly instantiated:
 | |
| 
 | |
| ```toml
 | |
| [environment]
 | |
| python-version = "3.12"
 | |
| ```
 | |
| 
 | |
| ```py
 | |
| from typing_extensions import Protocol, reveal_type
 | |
| 
 | |
| # error: [call-non-callable]
 | |
| reveal_type(Protocol())  # revealed: Unknown
 | |
| 
 | |
| class MyProtocol(Protocol):
 | |
|     x: int
 | |
| 
 | |
| # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
 | |
| reveal_type(MyProtocol())  # revealed: MyProtocol
 | |
| 
 | |
| class GenericProtocol[T](Protocol):
 | |
|     x: T
 | |
| 
 | |
| # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`"
 | |
| reveal_type(GenericProtocol[int]())  # revealed: GenericProtocol[int]
 | |
| ```
 | |
| 
 | |
| But a non-protocol class can be instantiated, even if it has `Protocol` in its MRO:
 | |
| 
 | |
| ```py
 | |
| class SubclassOfMyProtocol(MyProtocol): ...
 | |
| 
 | |
| reveal_type(SubclassOfMyProtocol())  # revealed: SubclassOfMyProtocol
 | |
| 
 | |
| class SubclassOfGenericProtocol[T](GenericProtocol[T]): ...
 | |
| 
 | |
| reveal_type(SubclassOfGenericProtocol[int]())  # revealed: SubclassOfGenericProtocol[int]
 | |
| ```
 | |
| 
 | |
| And as a corollary, `type[MyProtocol]` can also be called:
 | |
| 
 | |
| ```py
 | |
| def f(x: type[MyProtocol]):
 | |
|     # TODO: add a `reveal_type` call here once it's no longer a `Todo` type
 | |
|     # (which doesn't work well with snapshots)
 | |
|     x()
 | |
| ```
 | |
| 
 | |
| ## Members of a protocol
 | |
| 
 | |
| A protocol defines an interface through its *members*: if a protocol `Foo` has members `X` and `Y`,
 | |
| a type `Bar` can only be a subtype of `Foo` if inhabitants of `Bar` also have attributes `X` and
 | |
| `Y`.
 | |
| 
 | |
| A protocol class defines its members through declarations in the class body. The members of a
 | |
| protocol can be introspected using the function `typing.get_protocol_members`, which is backported
 | |
| via `typing_extensions`.
 | |
| 
 | |
| ```py
 | |
| from typing_extensions import Protocol, get_protocol_members
 | |
| 
 | |
| class Foo(Protocol):
 | |
|     x: int
 | |
| 
 | |
|     @property
 | |
|     def y(self) -> str:
 | |
|         return "y"
 | |
| 
 | |
|     @property
 | |
|     def z(self) -> int:
 | |
|         return 42
 | |
| 
 | |
|     @z.setter
 | |
|     def z(self, z: int) -> None: ...
 | |
|     def method_member(self) -> bytes:
 | |
|         return b"foo"
 | |
| 
 | |
| reveal_type(get_protocol_members(Foo))  # revealed: frozenset[Literal["method_member", "x", "y", "z"]]
 | |
| ```
 | |
| 
 | |
| To see the kinds and types of the protocol members, you can use the debugging aid
 | |
| `ty_extensions.reveal_protocol_interface`, meanwhile:
 | |
| 
 | |
| ```py
 | |
| from ty_extensions import reveal_protocol_interface
 | |
| from typing import SupportsIndex, SupportsAbs, ClassVar
 | |
| 
 | |
| # error: [revealed-type] "Revealed protocol interface: `{"method_member": MethodMember(`(self) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self) -> str` }, "z": PropertyMember { getter: `def z(self) -> int`, setter: `def z(self, z: int) -> None` }}`"
 | |
| reveal_protocol_interface(Foo)
 | |
| # error: [revealed-type] "Revealed protocol interface: `{"__index__": MethodMember(`(self) -> int`)}`"
 | |
| reveal_protocol_interface(SupportsIndex)
 | |
| # error: [revealed-type] "Revealed protocol interface: `{"__abs__": MethodMember(`(self) -> _T_co@SupportsAbs`)}`"
 | |
| reveal_protocol_interface(SupportsAbs)
 | |
| 
 | |
| # error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`"
 | |
| reveal_protocol_interface(int)
 | |
| # error: [invalid-argument-type] "Argument to function `reveal_protocol_interface` is incorrect: Expected `type`, found `Literal["foo"]`"
 | |
| reveal_protocol_interface("foo")
 | |
| 
 | |
| # TODO: this should be a `revealed-type` diagnostic rather than `invalid-argument-type`, and it should reveal `{"__abs__": MethodMember(`(self) -> int`)}` for the protocol interface
 | |
| #
 | |
| # error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`"
 | |
| reveal_protocol_interface(SupportsAbs[int])
 | |
| 
 | |
| class BaseProto(Protocol):
 | |
|     def member(self) -> int: ...
 | |
| 
 | |
| class SubProto(BaseProto, Protocol):
 | |
|     def member(self) -> bool: ...
 | |
| 
 | |
| # error: [revealed-type] "Revealed protocol interface: `{"member": MethodMember(`(self) -> int`)}`"
 | |
| reveal_protocol_interface(BaseProto)
 | |
| 
 | |
| # error: [revealed-type] "Revealed protocol interface: `{"member": MethodMember(`(self) -> bool`)}`"
 | |
| reveal_protocol_interface(SubProto)
 | |
| 
 | |
| class ProtoWithClassVar(Protocol):
 | |
|     x: ClassVar[int]
 | |
| 
 | |
| # error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`; ClassVar)}`"
 | |
| reveal_protocol_interface(ProtoWithClassVar)
 | |
| 
 | |
| class ProtocolWithDefault(Protocol):
 | |
|     x: int = 0
 | |
| 
 | |
| # We used to incorrectly report this as having an `x: Literal[0]` member;
 | |
| # declared types should take priority over inferred types for protocol interfaces!
 | |
| #
 | |
| # error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`)}`"
 | |
| reveal_protocol_interface(ProtocolWithDefault)
 | |
| ```
 | |
| 
 | |
| Certain special attributes and methods are not considered protocol members at runtime, and should
 | |
| not be considered protocol members by type checkers either:
 | |
| 
 | |
| ```py
 | |
| class Lumberjack(Protocol):
 | |
|     __slots__ = ()
 | |
|     __match_args__ = ()
 | |
|     _abc_foo: str  # any attribute starting with `_abc_` is excluded as a protocol attribute
 | |
|     x: int
 | |
| 
 | |
|     def __new__(cls, x: int) -> "Lumberjack":
 | |
|         return object.__new__(cls)
 | |
| 
 | |
|     def __init__(self, x: int) -> None:
 | |
|         self.x = x
 | |
| 
 | |
| reveal_type(get_protocol_members(Lumberjack))  # revealed: frozenset[Literal["x"]]
 | |
| ```
 | |
| 
 | |
| A sub-protocol inherits and extends the members of its superclass protocol(s):
 | |
| 
 | |
| ```py
 | |
| class Bar(Protocol):
 | |
|     spam: str
 | |
| 
 | |
| class Baz(Bar, Protocol):
 | |
|     ham: memoryview
 | |
| 
 | |
| reveal_type(get_protocol_members(Baz))  # revealed: frozenset[Literal["ham", "spam"]]
 | |
| 
 | |
| class Baz2(Bar, Foo, Protocol): ...
 | |
| 
 | |
| # revealed: frozenset[Literal["method_member", "spam", "x", "y", "z"]]
 | |
| reveal_type(get_protocol_members(Baz2))
 | |
| ```
 | |
| 
 | |
| ## Protocol members in statically known branches
 | |
| 
 | |
| <!-- snapshot-diagnostics -->
 | |
| 
 | |
| The list of protocol members does not include any members declared in branches that are statically
 | |
| known to be unreachable:
 | |
| 
 | |
| ```toml
 | |
| [environment]
 | |
| python-version = "3.9"
 | |
| ```
 | |
| 
 | |
| ```py
 | |
| import sys
 | |
| from typing_extensions import Protocol, get_protocol_members, reveal_type
 | |
| 
 | |
| class Foo(Protocol):
 | |
|     if sys.version_info >= (3, 10):
 | |
|         a: int
 | |
|         b = 42
 | |
|         def c(self) -> None: ...
 | |
|     else:
 | |
|         d: int
 | |
|         e = 56  # error: [ambiguous-protocol-member]
 | |
|         def f(self) -> None: ...
 | |
| 
 | |
| reveal_type(get_protocol_members(Foo))  # revealed: frozenset[Literal["d", "e", "f"]]
 | |
| ```
 | |
| 
 | |
| ## Invalid calls to `get_protocol_members()`
 | |
| 
 | |
| <!-- snapshot-diagnostics -->
 | |
| 
 | |
| Calling `get_protocol_members` on a non-protocol class raises an error at runtime:
 | |
| 
 | |
| ```toml
 | |
| [environment]
 | |
| python-version = "3.12"
 | |
| ```
 | |
| 
 | |
| ```py
 | |
| from typing_extensions import Protocol, get_protocol_members
 | |
| 
 | |
| class NotAProtocol: ...
 | |
| 
 | |
| get_protocol_members(NotAProtocol)  # error: [invalid-argument-type]
 | |
| 
 | |
| class AlsoNotAProtocol(NotAProtocol, object): ...
 | |
| 
 | |
| get_protocol_members(AlsoNotAProtocol)  # error: [invalid-argument-type]
 | |
| ```
 | |
| 
 | |
| The original class object must be passed to the function; a specialized version of a generic version
 | |
| does not suffice:
 | |
| 
 | |
| ```py
 | |
| class GenericProtocol[T](Protocol): ...
 | |
| 
 | |
| get_protocol_members(GenericProtocol[int])  # TODO: should emit a diagnostic here (https://github.com/astral-sh/ruff/issues/17549)
 | |
| ```
 | |
| 
 | |
| ## Subtyping of protocols with attribute members
 | |
| 
 | |
| In the following example, the protocol class `HasX` defines an interface such that any other fully
 | |
| static type can be said to be a subtype of `HasX` if all inhabitants of that other type have a
 | |
| mutable `x` attribute of type `int`:
 | |
| 
 | |
| ```toml
 | |
| [environment]
 | |
| python-version = "3.12"
 | |
| ```
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol, Any, ClassVar
 | |
| from collections.abc import Sequence
 | |
| from ty_extensions import static_assert, is_assignable_to, is_subtype_of
 | |
| 
 | |
| class HasX(Protocol):
 | |
|     x: int
 | |
| 
 | |
| class HasXY(Protocol):
 | |
|     x: int
 | |
|     y: int
 | |
| 
 | |
| class Foo:
 | |
|     x: int
 | |
| 
 | |
| static_assert(is_subtype_of(Foo, HasX))
 | |
| static_assert(is_assignable_to(Foo, HasX))
 | |
| static_assert(not is_subtype_of(Foo, HasXY))
 | |
| static_assert(not is_assignable_to(Foo, HasXY))
 | |
| 
 | |
| class FooSub(Foo): ...
 | |
| 
 | |
| static_assert(is_subtype_of(FooSub, HasX))
 | |
| static_assert(is_assignable_to(FooSub, HasX))
 | |
| static_assert(not is_subtype_of(FooSub, HasXY))
 | |
| static_assert(not is_assignable_to(FooSub, HasXY))
 | |
| 
 | |
| class FooBool(Foo):
 | |
|     x: bool
 | |
| 
 | |
| static_assert(not is_subtype_of(FooBool, HasX))
 | |
| static_assert(not is_assignable_to(FooBool, HasX))
 | |
| 
 | |
| class FooAny:
 | |
|     x: Any
 | |
| 
 | |
| static_assert(not is_subtype_of(FooAny, HasX))
 | |
| static_assert(is_assignable_to(FooAny, HasX))
 | |
| 
 | |
| class SubclassOfAny(Any): ...
 | |
| 
 | |
| class FooSubclassOfAny:
 | |
|     x: SubclassOfAny
 | |
| 
 | |
| static_assert(not is_subtype_of(FooSubclassOfAny, HasX))
 | |
| 
 | |
| # `FooSubclassOfAny` is assignable to `HasX` for the following reason. The `x` attribute on `FooSubclassOfAny`
 | |
| # is accessible on the class itself. When accessing `x` on an instance, the descriptor protocol is invoked, and
 | |
| # `__get__` is looked up on `SubclassOfAny`. Every member access on `SubclassOfAny` yields `Any`, so `__get__` is
 | |
| # also available, and calling `Any` also yields `Any`. Thus, accessing `x` on an instance of `FooSubclassOfAny`
 | |
| # yields `Any`, which is assignable to `int` and vice versa.
 | |
| static_assert(is_assignable_to(FooSubclassOfAny, HasX))
 | |
| 
 | |
| class FooWithY(Foo):
 | |
|     y: int
 | |
| 
 | |
| assert is_subtype_of(FooWithY, HasXY)
 | |
| static_assert(is_assignable_to(FooWithY, HasXY))
 | |
| 
 | |
| class Bar:
 | |
|     x: str
 | |
| 
 | |
| static_assert(not is_subtype_of(Bar, HasX))
 | |
| static_assert(not is_assignable_to(Bar, HasX))
 | |
| 
 | |
| class Baz:
 | |
|     y: int
 | |
| 
 | |
| static_assert(not is_subtype_of(Baz, HasX))
 | |
| static_assert(not is_assignable_to(Baz, HasX))
 | |
| 
 | |
| class Qux:
 | |
|     def __init__(self, x: int) -> None:
 | |
|         self.x: int = x
 | |
| 
 | |
| static_assert(is_subtype_of(Qux, HasX))
 | |
| static_assert(is_assignable_to(Qux, HasX))
 | |
| 
 | |
| class HalfUnknownQux:
 | |
|     def __init__(self, x: int) -> None:
 | |
|         self.x = x
 | |
| 
 | |
| reveal_type(HalfUnknownQux(1).x)  # revealed: Unknown | int
 | |
| 
 | |
| static_assert(not is_subtype_of(HalfUnknownQux, HasX))
 | |
| static_assert(is_assignable_to(HalfUnknownQux, HasX))
 | |
| 
 | |
| class FullyUnknownQux:
 | |
|     def __init__(self, x) -> None:
 | |
|         self.x = x
 | |
| 
 | |
| static_assert(not is_subtype_of(FullyUnknownQux, HasX))
 | |
| static_assert(is_assignable_to(FullyUnknownQux, HasX))
 | |
| 
 | |
| class HasXWithDefault(Protocol):
 | |
|     x: int = 0
 | |
| 
 | |
| class FooWithZero:
 | |
|     x: int = 0
 | |
| 
 | |
| static_assert(is_subtype_of(FooWithZero, HasXWithDefault))
 | |
| static_assert(is_assignable_to(FooWithZero, HasXWithDefault))
 | |
| 
 | |
| # TODO: whether or not any of these four assertions should pass is not clearly specified.
 | |
| #
 | |
| # A test in the typing conformance suite implies that they all should:
 | |
| # that a nominal class with an instance attribute `x`
 | |
| # (*without* a default value on the class body)
 | |
| # should be understood as satisfying a protocol that has an attribute member `x`
 | |
| # even if the protocol's `x` member has a default value on the class body.
 | |
| #
 | |
| # See <https://github.com/python/typing/blob/d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc/conformance/tests/protocols_definition.py#L56-L79>.
 | |
| #
 | |
| # The implications of this for meta-protocols are not clearly spelled out, however,
 | |
| # and the fact that attribute members on protocols can have defaults is only mentioned
 | |
| # in a throwaway comment in the spec's prose.
 | |
| static_assert(is_subtype_of(Foo, HasXWithDefault))
 | |
| static_assert(is_assignable_to(Foo, HasXWithDefault))
 | |
| static_assert(is_subtype_of(Qux, HasXWithDefault))
 | |
| static_assert(is_assignable_to(Qux, HasXWithDefault))
 | |
| 
 | |
| class HasClassVarX(Protocol):
 | |
|     x: ClassVar[int]
 | |
| 
 | |
| static_assert(is_subtype_of(FooWithZero, HasClassVarX))
 | |
| static_assert(is_assignable_to(FooWithZero, HasClassVarX))
 | |
| # TODO: these should pass
 | |
| static_assert(not is_subtype_of(Foo, HasClassVarX))  # error: [static-assert-error]
 | |
| static_assert(not is_assignable_to(Foo, HasClassVarX))  # error: [static-assert-error]
 | |
| static_assert(not is_subtype_of(Qux, HasClassVarX))  # error: [static-assert-error]
 | |
| static_assert(not is_assignable_to(Qux, HasClassVarX))  # error: [static-assert-error]
 | |
| 
 | |
| static_assert(is_subtype_of(Sequence[Foo], Sequence[HasX]))
 | |
| static_assert(is_assignable_to(Sequence[Foo], Sequence[HasX]))
 | |
| static_assert(not is_subtype_of(list[Foo], list[HasX]))
 | |
| static_assert(not is_assignable_to(list[Foo], list[HasX]))
 | |
| ```
 | |
| 
 | |
| Note that declaring an attribute member on a protocol mandates that the attribute must be mutable. A
 | |
| type with a read-only `x` property does not satisfy the `HasX` interface; nor does a type with a
 | |
| `Final` `x` attribute. The type of the attribute must also be treated as invariant due to the
 | |
| attribute's mutability:
 | |
| 
 | |
| ```py
 | |
| from typing import Final
 | |
| 
 | |
| class A:
 | |
|     @property
 | |
|     def x(self) -> int:
 | |
|         return 42
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(not is_subtype_of(A, HasX))  # error: [static-assert-error]
 | |
| static_assert(not is_assignable_to(A, HasX))  # error: [static-assert-error]
 | |
| 
 | |
| class B:
 | |
|     x: Final = 42
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(not is_subtype_of(A, HasX))  # error: [static-assert-error]
 | |
| static_assert(not is_assignable_to(A, HasX))  # error: [static-assert-error]
 | |
| 
 | |
| class IntSub(int): ...
 | |
| 
 | |
| class C:
 | |
|     x: IntSub
 | |
| 
 | |
| # due to invariance, a type is only a subtype of `HasX`
 | |
| # if its `x` attribute is of type *exactly* `int`:
 | |
| # a subclass of `int` does not satisfy the interface
 | |
| static_assert(not is_subtype_of(C, HasX))
 | |
| static_assert(not is_assignable_to(C, HasX))
 | |
| ```
 | |
| 
 | |
| All attributes on frozen dataclasses and namedtuples are immutable, so instances of these classes
 | |
| can never be considered to inhabit a protocol that declares a mutable-attribute member:
 | |
| 
 | |
| ```py
 | |
| from dataclasses import dataclass
 | |
| from typing import NamedTuple
 | |
| 
 | |
| @dataclass
 | |
| class MutableDataclass:
 | |
|     x: int
 | |
| 
 | |
| static_assert(is_subtype_of(MutableDataclass, HasX))
 | |
| static_assert(is_assignable_to(MutableDataclass, HasX))
 | |
| 
 | |
| @dataclass(frozen=True)
 | |
| class ImmutableDataclass:
 | |
|     x: int
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(not is_subtype_of(ImmutableDataclass, HasX))  # error: [static-assert-error]
 | |
| static_assert(not is_assignable_to(ImmutableDataclass, HasX))  # error: [static-assert-error]
 | |
| 
 | |
| class NamedTupleWithX(NamedTuple):
 | |
|     x: int
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(not is_subtype_of(NamedTupleWithX, HasX))  # error: [static-assert-error]
 | |
| static_assert(not is_assignable_to(NamedTupleWithX, HasX))  # error: [static-assert-error]
 | |
| ```
 | |
| 
 | |
| However, a type with a read-write property `x` *does* satisfy the `HasX` protocol. The `HasX`
 | |
| protocol only specifies what the type of `x` should be when accessed from instances; instances of
 | |
| `XProperty` in the below example have a mutable attribute `x` of type `int`:
 | |
| 
 | |
| ```py
 | |
| class XProperty:
 | |
|     _x: int
 | |
| 
 | |
|     @property
 | |
|     def x(self) -> int:
 | |
|         return self._x
 | |
| 
 | |
|     @x.setter
 | |
|     def x(self, x: int) -> None:
 | |
|         self._x = x**2
 | |
| 
 | |
| static_assert(is_subtype_of(XProperty, HasX))
 | |
| static_assert(is_assignable_to(XProperty, HasX))
 | |
| ```
 | |
| 
 | |
| Attribute members on protocol classes are allowed to have default values, just like instance
 | |
| attributes on other classes. Similar to nominal classes, attributes with defaults can be accessed on
 | |
| the class object itself and any explicit subclasses of the protocol class. It cannot be assumed to
 | |
| exist on the meta-type of any arbitrary inhabitant of the protocol type, however; an implicit
 | |
| subtype of the protocol will not necessarily have a default value for the instance attribute
 | |
| provided in its class body:
 | |
| 
 | |
| ```py
 | |
| class HasXWithDefault(Protocol):
 | |
|     x: int = 42
 | |
| 
 | |
| reveal_type(HasXWithDefault.x)  # revealed: int
 | |
| 
 | |
| class ExplicitSubclass(HasXWithDefault): ...
 | |
| 
 | |
| reveal_type(ExplicitSubclass.x)  # revealed: int
 | |
| 
 | |
| def f(arg: HasXWithDefault):
 | |
|     # TODO: should emit `[unresolved-reference]` and reveal `Unknown`
 | |
|     reveal_type(type(arg).x)  # revealed: int
 | |
| ```
 | |
| 
 | |
| Assignments in a class body of a protocol -- of any kind -- are not permitted by ty unless the
 | |
| symbol being assigned to is also explicitly declared in the body of the protocol class or one of its
 | |
| superclasses. Note that this is stricter validation of protocol members than many other type
 | |
| checkers currently apply (as of 2025/04/21).
 | |
| 
 | |
| The reason for this strict validation is that undeclared variables in the class body would lead to
 | |
| an ambiguous interface being declared by the protocol.
 | |
| 
 | |
| ```py
 | |
| from typing_extensions import TypeAlias, get_protocol_members
 | |
| 
 | |
| class MyContext:
 | |
|     def __enter__(self) -> int:
 | |
|         return 42
 | |
| 
 | |
|     def __exit__(self, *args) -> None: ...
 | |
| 
 | |
| class LotsOfBindings(Protocol):
 | |
|     a: int
 | |
|     a = 42  # this is fine, since `a` is declared in the class body
 | |
|     b: int = 56  # this is also fine, by the same principle
 | |
| 
 | |
|     type c = str  # this is very strange but I can't see a good reason to disallow it
 | |
|     d: TypeAlias = bytes  # same here
 | |
| 
 | |
|     class Nested: ...  # also weird, but we should also probably allow it
 | |
|     class NestedProtocol(Protocol): ...  # same here...
 | |
|     e = 72  # error: [ambiguous-protocol-member]
 | |
| 
 | |
|     # error: [ambiguous-protocol-member] "Consider adding an annotation, e.g. `f: int = ...`"
 | |
|     # error: [ambiguous-protocol-member] "Consider adding an annotation, e.g. `g: int = ...`"
 | |
|     f, g = (1, 2)
 | |
| 
 | |
|     h: int = (i := 3)  # error: [ambiguous-protocol-member]
 | |
| 
 | |
|     for j in range(42):  # error: [ambiguous-protocol-member]
 | |
|         pass
 | |
| 
 | |
|     with MyContext() as k:  # error: [ambiguous-protocol-member]
 | |
|         pass
 | |
| 
 | |
|     match object():
 | |
|         case l:  # error: [ambiguous-protocol-member]
 | |
|             ...
 | |
| 
 | |
| # revealed: frozenset[Literal["Nested", "NestedProtocol", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"]]
 | |
| reveal_type(get_protocol_members(LotsOfBindings))
 | |
| 
 | |
| class Foo(Protocol):
 | |
|     a: int
 | |
| 
 | |
| class Bar(Foo, Protocol):
 | |
|     a = 42  # fine, because it's declared in the superclass
 | |
| 
 | |
| reveal_type(get_protocol_members(Bar))  # revealed: frozenset[Literal["a"]]
 | |
| ```
 | |
| 
 | |
| A binding-without-declaration will not be reported if it occurs in a branch that we can statically
 | |
| determine to be unreachable. The reason is that we don't consider it to be a protocol member at all
 | |
| if all definitions for the variable are in unreachable blocks:
 | |
| 
 | |
| ```py
 | |
| import sys
 | |
| 
 | |
| class Protocol694(Protocol):
 | |
|     if sys.version_info > (3, 694):
 | |
|         x = 42  # no error!
 | |
| ```
 | |
| 
 | |
| If there are multiple bindings of the variable in the class body, however, and at least one of the
 | |
| bindings occurs in a block of code that is understood to be (possibly) reachable, a diagnostic will
 | |
| be reported. The diagnostic will be attached to the first binding that occurs in the class body,
 | |
| even if that first definition occurs in an unreachable block:
 | |
| 
 | |
| ```py
 | |
| class Protocol695(Protocol):
 | |
|     if sys.version_info > (3, 695):
 | |
|         x = 42
 | |
|     else:
 | |
|         x = 42
 | |
| 
 | |
|     x = 56  # error: [ambiguous-protocol-member]
 | |
| ```
 | |
| 
 | |
| In order for the variable to be considered declared, the declaration of the variable must also take
 | |
| place in a block of code that is understood to be (possibly) reachable:
 | |
| 
 | |
| ```py
 | |
| class Protocol696(Protocol):
 | |
|     if sys.version_info > (3, 696):
 | |
|         x: int
 | |
|     else:
 | |
|         x = 42  # error: [ambiguous-protocol-member]
 | |
|         y: int
 | |
| 
 | |
|     y = 56  # no error
 | |
| ```
 | |
| 
 | |
| Attribute members are allowed to have assignments in methods on the protocol class, just like
 | |
| non-protocol classes. Unlike other classes, however, instance attributes that are not declared in
 | |
| the class body are disallowed. This is mandated by [the spec][spec_protocol_members]:
 | |
| 
 | |
| > Additional attributes *only* defined in the body of a method by assignment via `self` are not
 | |
| > allowed. The rationale for this is that the protocol class implementation is often not shared by
 | |
| > subtypes, so the interface should not depend on the default implementation.
 | |
| 
 | |
| ```py
 | |
| class Foo(Protocol):
 | |
|     x: int
 | |
|     y: str
 | |
| 
 | |
|     def __init__(self) -> None:
 | |
|         self.x = 42  # fine
 | |
|         self.a = 56  # TODO: should emit diagnostic
 | |
|         self.b: int = 128  # TODO: should emit diagnostic
 | |
| 
 | |
|     def non_init_method(self) -> None:
 | |
|         self.y = 64  # fine
 | |
|         self.c = 72  # TODO: should emit diagnostic
 | |
| 
 | |
| # Note: the list of members does not include `a`, `b` or `c`,
 | |
| # as none of these attributes is declared in the class body.
 | |
| reveal_type(get_protocol_members(Foo))  # revealed: frozenset[Literal["non_init_method", "x", "y"]]
 | |
| ```
 | |
| 
 | |
| If a member is declared in a superclass of a protocol class, it is fine for it to be assigned to in
 | |
| the sub-protocol class without a redeclaration:
 | |
| 
 | |
| ```py
 | |
| class Super(Protocol):
 | |
|     x: int
 | |
| 
 | |
| class Sub(Super, Protocol):
 | |
|     x = 42  # no error here, since it's declared in the superclass
 | |
| 
 | |
| reveal_type(get_protocol_members(Super))  # revealed: frozenset[Literal["x"]]
 | |
| reveal_type(get_protocol_members(Sub))  # revealed: frozenset[Literal["x"]]
 | |
| ```
 | |
| 
 | |
| If a protocol has 0 members, then all other types are assignable to it, and all fully static types
 | |
| are subtypes of it:
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol
 | |
| 
 | |
| class UniversalSet(Protocol): ...
 | |
| 
 | |
| static_assert(is_assignable_to(object, UniversalSet))
 | |
| static_assert(is_subtype_of(object, UniversalSet))
 | |
| ```
 | |
| 
 | |
| Which means that `UniversalSet` here is in fact an equivalent type to `object`:
 | |
| 
 | |
| ```py
 | |
| from ty_extensions import is_equivalent_to
 | |
| 
 | |
| static_assert(is_equivalent_to(UniversalSet, object))
 | |
| ```
 | |
| 
 | |
| `object` is a subtype of certain other protocols too. Since all fully static types (whether nominal
 | |
| or structural) are subtypes of `object`, these protocols are also subtypes of `object`; and this
 | |
| means that these protocols are also equivalent to `UniversalSet` and `object`:
 | |
| 
 | |
| ```py
 | |
| class SupportsStr(Protocol):
 | |
|     def __str__(self) -> str: ...
 | |
| 
 | |
| static_assert(is_equivalent_to(SupportsStr, UniversalSet))
 | |
| static_assert(is_equivalent_to(SupportsStr, object))
 | |
| 
 | |
| class SupportsClass(Protocol):
 | |
|     @property
 | |
|     def __class__(self) -> type: ...
 | |
| 
 | |
| static_assert(is_equivalent_to(SupportsClass, UniversalSet))
 | |
| static_assert(is_equivalent_to(SupportsClass, SupportsStr))
 | |
| static_assert(is_equivalent_to(SupportsClass, object))
 | |
| ```
 | |
| 
 | |
| If a protocol contains members that are not defined on `object`, then that protocol will (like all
 | |
| types in Python) still be assignable to `object`, but `object` will not be assignable to that
 | |
| protocol:
 | |
| 
 | |
| ```py
 | |
| static_assert(is_assignable_to(HasX, object))
 | |
| static_assert(is_subtype_of(HasX, object))
 | |
| static_assert(not is_assignable_to(object, HasX))
 | |
| static_assert(not is_subtype_of(object, HasX))
 | |
| ```
 | |
| 
 | |
| But `object` is the *only* fully static nominal type that a protocol type can ever be assignable to
 | |
| or a subtype of:
 | |
| 
 | |
| ```py
 | |
| static_assert(not is_assignable_to(HasX, Foo))
 | |
| static_assert(not is_subtype_of(HasX, Foo))
 | |
| ```
 | |
| 
 | |
| ## Diagnostics for protocols with invalid attribute members
 | |
| 
 | |
| This is a short appendix to the previous section with the `snapshot-diagnostics` directive enabled
 | |
| (enabling snapshots for the previous section in its entirety would lead to a huge snapshot, since
 | |
| it's a large section).
 | |
| 
 | |
| <!-- snapshot-diagnostics -->
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol
 | |
| 
 | |
| def coinflip() -> bool:
 | |
|     return True
 | |
| 
 | |
| class A(Protocol):
 | |
|     # The `x` and `y` members attempt to use Python-2-style type comments
 | |
|     # to indicate that the type should be `int | None` and `str` respectively,
 | |
|     # but we don't support those
 | |
| 
 | |
|     # error: [ambiguous-protocol-member]
 | |
|     a = None  # type: int
 | |
|     # error: [ambiguous-protocol-member]
 | |
|     b = ...  # type: str
 | |
| 
 | |
|     if coinflip():
 | |
|         c = 1  # error: [ambiguous-protocol-member]
 | |
|     else:
 | |
|         c = 2
 | |
| 
 | |
|     # error: [ambiguous-protocol-member]
 | |
|     for d in range(42):
 | |
|         pass
 | |
| ```
 | |
| 
 | |
| ## Equivalence of protocols
 | |
| 
 | |
| Two protocols are considered equivalent types if they specify the same interface, even if they have
 | |
| different names:
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol
 | |
| from ty_extensions import is_equivalent_to, static_assert
 | |
| 
 | |
| class HasX(Protocol):
 | |
|     x: int
 | |
| 
 | |
| class AlsoHasX(Protocol):
 | |
|     x: int
 | |
| 
 | |
| static_assert(is_equivalent_to(HasX, AlsoHasX))
 | |
| ```
 | |
| 
 | |
| And unions containing equivalent protocols are recognized as equivalent, even when the order is not
 | |
| identical:
 | |
| 
 | |
| ```py
 | |
| class HasY(Protocol):
 | |
|     y: str
 | |
| 
 | |
| class AlsoHasY(Protocol):
 | |
|     y: str
 | |
| 
 | |
| class A: ...
 | |
| class B: ...
 | |
| 
 | |
| static_assert(is_equivalent_to(A | HasX | B | HasY, B | AlsoHasY | AlsoHasX | A))
 | |
| ```
 | |
| 
 | |
| Protocols are considered equivalent if their members are equivalent, even if those members are
 | |
| differently ordered unions:
 | |
| 
 | |
| ```py
 | |
| class C: ...
 | |
| 
 | |
| class UnionProto1(Protocol):
 | |
|     x: A | B | C
 | |
| 
 | |
| class UnionProto2(Protocol):
 | |
|     x: C | A | B
 | |
| 
 | |
| static_assert(is_equivalent_to(UnionProto1, UnionProto2))
 | |
| static_assert(is_equivalent_to(UnionProto1 | A | B, B | UnionProto2 | A))
 | |
| ```
 | |
| 
 | |
| ## Intersections of protocols
 | |
| 
 | |
| An intersection of two protocol types `X` and `Y` is equivalent to a protocol type `Z` that inherits
 | |
| from both `X` and `Y`:
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol
 | |
| from ty_extensions import Intersection, static_assert, is_equivalent_to
 | |
| 
 | |
| class HasX(Protocol):
 | |
|     x: int
 | |
| 
 | |
| class HasY(Protocol):
 | |
|     y: str
 | |
| 
 | |
| class HasXAndYProto(HasX, HasY, Protocol): ...
 | |
| 
 | |
| # TODO: this should pass
 | |
| static_assert(is_equivalent_to(HasXAndYProto, Intersection[HasX, HasY]))  # error: [static-assert-error]
 | |
| ```
 | |
| 
 | |
| But this is only true if the subclass has `Protocol` in its explicit bases (otherwise, it is a
 | |
| nominal type rather than a structural type):
 | |
| 
 | |
| ```py
 | |
| class HasXAndYNominal(HasX, HasY): ...
 | |
| 
 | |
| static_assert(not is_equivalent_to(HasXAndYNominal, Intersection[HasX, HasY]))
 | |
| ```
 | |
| 
 | |
| A protocol type `X` and a nominal type `Y` can be inferred as disjoint types if `Y` is a `@final`
 | |
| type and `Y` does not satisfy the interface declared by `X`. But if `Y` is not `@final`, then this
 | |
| does not hold true, since a subclass of `Y` could always provide additional methods or attributes
 | |
| that would lead to it satisfying `X`'s interface:
 | |
| 
 | |
| ```py
 | |
| from typing import final
 | |
| from ty_extensions import is_disjoint_from
 | |
| 
 | |
| class NotFinalNominal: ...
 | |
| 
 | |
| @final
 | |
| class FinalNominal: ...
 | |
| 
 | |
| static_assert(not is_disjoint_from(NotFinalNominal, HasX))
 | |
| static_assert(is_disjoint_from(FinalNominal, HasX))
 | |
| 
 | |
| def _(arg1: Intersection[HasX, NotFinalNominal], arg2: Intersection[HasX, FinalNominal]):
 | |
|     reveal_type(arg1)  # revealed: HasX & NotFinalNominal
 | |
|     reveal_type(arg2)  # revealed: Never
 | |
| ```
 | |
| 
 | |
| The disjointness of a single protocol member with the type of an attribute on another type is enough
 | |
| to make the whole protocol disjoint from the other type, even if all other members on the protocol
 | |
| are satisfied by the other type. This applies to both `@final` types and non-final types:
 | |
| 
 | |
| ```py
 | |
| class Proto(Protocol):
 | |
|     x: int
 | |
|     y: str
 | |
|     z: bytes
 | |
| 
 | |
| class Foo:
 | |
|     x: int
 | |
|     y: str
 | |
|     z: None
 | |
| 
 | |
| static_assert(is_disjoint_from(Proto, Foo))
 | |
| 
 | |
| @final
 | |
| class FinalFoo:
 | |
|     x: int
 | |
|     y: str
 | |
|     z: None
 | |
| 
 | |
| static_assert(is_disjoint_from(Proto, FinalFoo))
 | |
| ```
 | |
| 
 | |
| ## Intersections of protocols with types that have possibly unbound attributes
 | |
| 
 | |
| Note that if a `@final` class has a possibly unbound attribute corresponding to the protocol member,
 | |
| instance types and class-literal types referring to that class cannot be a subtype of the protocol
 | |
| but will also not be disjoint from the protocol:
 | |
| 
 | |
| `a.py`:
 | |
| 
 | |
| ```py
 | |
| from typing import final, ClassVar, Protocol
 | |
| from ty_extensions import TypeOf, static_assert, is_subtype_of, is_disjoint_from, is_assignable_to
 | |
| 
 | |
| def who_knows() -> bool:
 | |
|     return False
 | |
| 
 | |
| @final
 | |
| class Foo:
 | |
|     if who_knows():
 | |
|         x: ClassVar[int] = 42
 | |
| 
 | |
| class HasReadOnlyX(Protocol):
 | |
|     @property
 | |
|     def x(self) -> int: ...
 | |
| 
 | |
| static_assert(not is_subtype_of(Foo, HasReadOnlyX))
 | |
| static_assert(not is_assignable_to(Foo, HasReadOnlyX))
 | |
| static_assert(not is_disjoint_from(Foo, HasReadOnlyX))
 | |
| 
 | |
| static_assert(not is_subtype_of(type[Foo], HasReadOnlyX))
 | |
| static_assert(not is_assignable_to(type[Foo], HasReadOnlyX))
 | |
| static_assert(not is_disjoint_from(type[Foo], HasReadOnlyX))
 | |
| 
 | |
| static_assert(not is_subtype_of(TypeOf[Foo], HasReadOnlyX))
 | |
| static_assert(not is_assignable_to(TypeOf[Foo], HasReadOnlyX))
 | |
| static_assert(not is_disjoint_from(TypeOf[Foo], HasReadOnlyX))
 | |
| ```
 | |
| 
 | |
| A similar principle applies to module-literal types that have possibly unbound attributes:
 | |
| 
 | |
| `b.py`:
 | |
| 
 | |
| ```py
 | |
| def who_knows() -> bool:
 | |
|     return False
 | |
| 
 | |
| if who_knows():
 | |
|     x: int = 42
 | |
| ```
 | |
| 
 | |
| `c.py`:
 | |
| 
 | |
| ```py
 | |
| import b
 | |
| from a import HasReadOnlyX
 | |
| from ty_extensions import TypeOf, static_assert, is_subtype_of, is_disjoint_from, is_assignable_to
 | |
| 
 | |
| static_assert(not is_subtype_of(TypeOf[b], HasReadOnlyX))
 | |
| static_assert(not is_assignable_to(TypeOf[b], HasReadOnlyX))
 | |
| static_assert(not is_disjoint_from(TypeOf[b], HasReadOnlyX))
 | |
| ```
 | |
| 
 | |
| If the possibly unbound attribute's type is disjoint from the type of the protocol member, though,
 | |
| it is still disjoint from the protocol. This applies to both `@final` types and non-final types:
 | |
| 
 | |
| `d.py`:
 | |
| 
 | |
| ```py
 | |
| from a import HasReadOnlyX, who_knows
 | |
| from typing import final, ClassVar, Protocol
 | |
| from ty_extensions import static_assert, is_disjoint_from, TypeOf
 | |
| 
 | |
| class Proto(Protocol):
 | |
|     x: int
 | |
| 
 | |
| class Foo:
 | |
|     def __init__(self):
 | |
|         if who_knows():
 | |
|             self.x: None = None
 | |
| 
 | |
| @final
 | |
| class FinalFoo:
 | |
|     def __init__(self):
 | |
|         if who_knows():
 | |
|             self.x: None = None
 | |
| 
 | |
| static_assert(is_disjoint_from(Foo, Proto))
 | |
| static_assert(is_disjoint_from(FinalFoo, Proto))
 | |
| ```
 | |
| 
 | |
| ## Satisfying a protocol's interface
 | |
| 
 | |
| A type does not have to be an `Instance` type in order to be a subtype of a protocol. Other
 | |
| protocols can be a subtype of a protocol, as can `ModuleLiteral` types, `ClassLiteral` types, and
 | |
| others. Another protocol can be a subtype of `HasX` either through "explicit" (nominal) inheritance
 | |
| from `HasX`, or by specifying a superset of `HasX`'s interface:
 | |
| 
 | |
| `module.py`:
 | |
| 
 | |
| ```py
 | |
| x: int = 42
 | |
| ```
 | |
| 
 | |
| `main.py`:
 | |
| 
 | |
| ```py
 | |
| import module
 | |
| from typing import Protocol
 | |
| from ty_extensions import is_subtype_of, is_assignable_to, static_assert, TypeOf
 | |
| 
 | |
| class HasX(Protocol):
 | |
|     x: int
 | |
| 
 | |
| static_assert(is_subtype_of(TypeOf[module], HasX))
 | |
| static_assert(is_assignable_to(TypeOf[module], HasX))
 | |
| 
 | |
| class ExplicitProtocolSubtype(HasX, Protocol):
 | |
|     y: int
 | |
| 
 | |
| static_assert(is_subtype_of(ExplicitProtocolSubtype, HasX))
 | |
| static_assert(is_assignable_to(ExplicitProtocolSubtype, HasX))
 | |
| 
 | |
| class ImplicitProtocolSubtype(Protocol):
 | |
|     x: int
 | |
|     y: str
 | |
| 
 | |
| static_assert(is_subtype_of(ImplicitProtocolSubtype, HasX))
 | |
| static_assert(is_assignable_to(ImplicitProtocolSubtype, HasX))
 | |
| 
 | |
| class Meta(type):
 | |
|     x: int
 | |
| 
 | |
| class UsesMeta(metaclass=Meta): ...
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(is_subtype_of(UsesMeta, HasX))  # error: [static-assert-error]
 | |
| static_assert(is_assignable_to(UsesMeta, HasX))  # error: [static-assert-error]
 | |
| ```
 | |
| 
 | |
| ## `ClassVar` attribute members
 | |
| 
 | |
| If a protocol `ClassVarX` has a `ClassVar` attribute member `x` with type `int`, this indicates that
 | |
| a readable `x` attribute must be accessible on any inhabitant of `ClassVarX`, and that a readable
 | |
| `x` attribute must *also* be accessible on the *type* of that inhabitant:
 | |
| 
 | |
| `classvars.py`:
 | |
| 
 | |
| ```py
 | |
| from typing import ClassVar, Protocol
 | |
| from ty_extensions import is_subtype_of, is_assignable_to, static_assert
 | |
| 
 | |
| class ClassVarXProto(Protocol):
 | |
|     x: ClassVar[int]
 | |
| 
 | |
| def f(obj: ClassVarXProto):
 | |
|     reveal_type(obj.x)  # revealed: int
 | |
|     reveal_type(type(obj).x)  # revealed: int
 | |
|     obj.x = 42  # error: [invalid-attribute-access] "Cannot assign to ClassVar `x` from an instance of type `ClassVarXProto`"
 | |
| 
 | |
| class InstanceAttrX:
 | |
|     x: int
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto))  # error: [static-assert-error]
 | |
| static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto))  # error: [static-assert-error]
 | |
| 
 | |
| class PropertyX:
 | |
|     @property
 | |
|     def x(self) -> int:
 | |
|         return 42
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(not is_assignable_to(PropertyX, ClassVarXProto))  # error: [static-assert-error]
 | |
| static_assert(not is_subtype_of(PropertyX, ClassVarXProto))  # error: [static-assert-error]
 | |
| 
 | |
| class ClassVarX:
 | |
|     x: ClassVar[int] = 42
 | |
| 
 | |
| static_assert(is_assignable_to(ClassVarX, ClassVarXProto))
 | |
| static_assert(is_subtype_of(ClassVarX, ClassVarXProto))
 | |
| ```
 | |
| 
 | |
| This is mentioned by the
 | |
| [spec](https://typing.python.org/en/latest/spec/protocol.html#protocol-members) and tested in the
 | |
| [conformance suite](https://github.com/python/typing/blob/main/conformance/tests/protocols_definition.py)
 | |
| as something that must be supported by type checkers:
 | |
| 
 | |
| > To distinguish between protocol class variables and protocol instance variables, the special
 | |
| > `ClassVar` annotation should be used.
 | |
| 
 | |
| ## Subtyping of protocols with property members
 | |
| 
 | |
| A read-only property on a protocol can be satisfied by a mutable attribute, a read-only property, a
 | |
| read/write property, a `Final` attribute, or a `ClassVar` attribute:
 | |
| 
 | |
| ```py
 | |
| from typing import ClassVar, Final, Protocol
 | |
| from ty_extensions import is_subtype_of, is_assignable_to, static_assert
 | |
| 
 | |
| class HasXProperty(Protocol):
 | |
|     @property
 | |
|     def x(self) -> int: ...
 | |
| 
 | |
| class XAttr:
 | |
|     x: int
 | |
| 
 | |
| static_assert(is_subtype_of(XAttr, HasXProperty))
 | |
| static_assert(is_assignable_to(XAttr, HasXProperty))
 | |
| 
 | |
| class XReadProperty:
 | |
|     @property
 | |
|     def x(self) -> int:
 | |
|         return 42
 | |
| 
 | |
| static_assert(is_subtype_of(XReadProperty, HasXProperty))
 | |
| static_assert(is_assignable_to(XReadProperty, HasXProperty))
 | |
| 
 | |
| class XReadWriteProperty:
 | |
|     @property
 | |
|     def x(self) -> int:
 | |
|         return 42
 | |
| 
 | |
|     @x.setter
 | |
|     def x(self, val: int) -> None: ...
 | |
| 
 | |
| static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))
 | |
| static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))
 | |
| 
 | |
| class XClassVar:
 | |
|     x: ClassVar[int] = 42
 | |
| 
 | |
| static_assert(is_subtype_of(XClassVar, HasXProperty))
 | |
| static_assert(is_assignable_to(XClassVar, HasXProperty))
 | |
| 
 | |
| class XFinal:
 | |
|     x: Final = 42
 | |
| 
 | |
| static_assert(is_subtype_of(XFinal, HasXProperty))
 | |
| static_assert(is_assignable_to(XFinal, HasXProperty))
 | |
| ```
 | |
| 
 | |
| A read-only property on a protocol, unlike a mutable attribute, is covariant: `XSub` in the below
 | |
| example satisfies the `HasXProperty` interface even though the type of the `x` attribute on `XSub`
 | |
| is a subtype of `int` rather than being exactly `int`.
 | |
| 
 | |
| ```py
 | |
| class MyInt(int): ...
 | |
| 
 | |
| class XSub:
 | |
|     x: MyInt
 | |
| 
 | |
| static_assert(is_subtype_of(XSub, HasXProperty))
 | |
| static_assert(is_assignable_to(XSub, HasXProperty))
 | |
| ```
 | |
| 
 | |
| A read/write property on a protocol, where the getter returns the same type that the setter takes,
 | |
| is equivalent to a normal mutable attribute on a protocol.
 | |
| 
 | |
| ```py
 | |
| class HasMutableXProperty(Protocol):
 | |
|     @property
 | |
|     def x(self) -> int: ...
 | |
|     @x.setter
 | |
|     def x(self, val: int) -> None: ...
 | |
| 
 | |
| class XAttr:
 | |
|     x: int
 | |
| 
 | |
| static_assert(is_subtype_of(XAttr, HasXProperty))
 | |
| static_assert(is_assignable_to(XAttr, HasXProperty))
 | |
| 
 | |
| class XReadProperty:
 | |
|     @property
 | |
|     def x(self) -> int:
 | |
|         return 42
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(not is_subtype_of(XReadProperty, HasXProperty))  # error: [static-assert-error]
 | |
| static_assert(not is_assignable_to(XReadProperty, HasXProperty))  # error: [static-assert-error]
 | |
| 
 | |
| class XReadWriteProperty:
 | |
|     @property
 | |
|     def x(self) -> int:
 | |
|         return 42
 | |
| 
 | |
|     @x.setter
 | |
|     def x(self, val: int) -> None: ...
 | |
| 
 | |
| static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))
 | |
| static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))
 | |
| 
 | |
| class XSub:
 | |
|     x: MyInt
 | |
| 
 | |
| # TODO: should pass
 | |
| static_assert(not is_subtype_of(XSub, HasXProperty))  # error: [static-assert-error]
 | |
| static_assert(not is_assignable_to(XSub, HasXProperty))  # error: [static-assert-error]
 | |
| ```
 | |
| 
 | |
| A protocol with a read/write property `x` is exactly equivalent to a protocol with a mutable
 | |
| attribute `x`. Both are subtypes of a protocol with a read-only prooperty `x`:
 | |
| 
 | |
| ```py
 | |
| from ty_extensions import is_equivalent_to
 | |
| 
 | |
| class HasMutableXAttr(Protocol):
 | |
|     x: int
 | |
| 
 | |
| # TODO: should pass
 | |
| static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty))  # error: [static-assert-error]
 | |
| 
 | |
| static_assert(is_subtype_of(HasMutableXAttr, HasXProperty))
 | |
| static_assert(is_assignable_to(HasMutableXAttr, HasXProperty))
 | |
| 
 | |
| static_assert(is_subtype_of(HasMutableXProperty, HasXProperty))
 | |
| static_assert(is_assignable_to(HasMutableXProperty, HasXProperty))
 | |
| ```
 | |
| 
 | |
| A read/write property on a protocol, where the setter accepts a subtype of the type returned by the
 | |
| getter, can be satisfied by a mutable attribute of any type bounded by the upper bound of the
 | |
| getter-returned type and the lower bound of the setter-accepted type.
 | |
| 
 | |
| This follows from the principle that a type `X` can only be a subtype of a given protocol if the
 | |
| `X`'s behavior is a superset of the behavior specified by the interface declared by the protocol. In
 | |
| the below example, the behavior of an instance of `XAttr` is a superset of the behavior specified by
 | |
| the protocol `HasAsymmetricXProperty`. The protocol specifies that reading an `x` attribute on the
 | |
| instance must resolve to an instance of `int` or a subclass thereof, and `XAttr` satisfies this
 | |
| requirement. The protocol also specifies that you must be able to assign instances of `MyInt` to the
 | |
| `x` attribute, and again this is satisfied by `XAttr`: on instances of `XAttr`, you can assign *any*
 | |
| instance of `int` to the `x` attribute, and thus by extension you can assign any instance of
 | |
| `IntSub` to the `x` attribute, since any instance of `IntSub` is an instance of `int`:
 | |
| 
 | |
| ```py
 | |
| class HasAsymmetricXProperty(Protocol):
 | |
|     @property
 | |
|     def x(self) -> int: ...
 | |
|     @x.setter
 | |
|     def x(self, val: MyInt) -> None: ...
 | |
| 
 | |
| class XAttr:
 | |
|     x: int
 | |
| 
 | |
| static_assert(is_subtype_of(XAttr, HasAsymmetricXProperty))
 | |
| static_assert(is_assignable_to(XAttr, HasAsymmetricXProperty))
 | |
| ```
 | |
| 
 | |
| The end conclusion of this is that the getter-returned type of a property is always covariant and
 | |
| the setter-accepted type is always contravariant. The combination of these leads to invariance for a
 | |
| regular mutable attribute, where the implied getter-returned and setter-accepted types are the same.
 | |
| 
 | |
| ```py
 | |
| class XAttrSub:
 | |
|     x: MyInt
 | |
| 
 | |
| static_assert(is_subtype_of(XAttrSub, HasAsymmetricXProperty))
 | |
| static_assert(is_assignable_to(XAttrSub, HasAsymmetricXProperty))
 | |
| 
 | |
| class MyIntSub(MyInt):
 | |
|     pass
 | |
| 
 | |
| class XAttrSubSub:
 | |
|     x: MyIntSub
 | |
| 
 | |
| # TODO: should pass
 | |
| static_assert(not is_subtype_of(XAttrSubSub, HasAsymmetricXProperty))  # error: [static-assert-error]
 | |
| static_assert(not is_assignable_to(XAttrSubSub, HasAsymmetricXProperty))  # error: [static-assert-error]
 | |
| ```
 | |
| 
 | |
| An asymmetric property on a protocol can also be satisfied by an asymmetric property on a nominal
 | |
| class whose getter and setter types satisfy the covariant and contravariant requirements,
 | |
| respectively.
 | |
| 
 | |
| ```py
 | |
| class XAsymmetricProperty:
 | |
|     @property
 | |
|     def x(self) -> MyInt:
 | |
|         return MyInt(0)
 | |
| 
 | |
|     @x.setter
 | |
|     def x(self, x: int) -> None: ...
 | |
| 
 | |
| static_assert(is_subtype_of(XAsymmetricProperty, HasAsymmetricXProperty))
 | |
| static_assert(is_assignable_to(XAsymmetricProperty, HasAsymmetricXProperty))
 | |
| ```
 | |
| 
 | |
| A custom descriptor attribute on the nominal class will also suffice:
 | |
| 
 | |
| ```py
 | |
| class Descriptor:
 | |
|     def __get__(self, instance, owner) -> MyInt:
 | |
|         return MyInt(0)
 | |
| 
 | |
|     def __set__(self, value: int) -> None: ...
 | |
| 
 | |
| class XCustomDescriptor:
 | |
|     x: Descriptor = Descriptor()
 | |
| 
 | |
| static_assert(is_subtype_of(XCustomDescriptor, HasAsymmetricXProperty))
 | |
| static_assert(is_assignable_to(XCustomDescriptor, HasAsymmetricXProperty))
 | |
| ```
 | |
| 
 | |
| Moreover, a read-only property on a protocol can be satisfied by a nominal class that defines a
 | |
| `__getattr__` method returning a suitable type. A read/write property can be satisfied by a nominal
 | |
| class that defines a `__getattr__` method returning a suitable type *and* a `__setattr__` method
 | |
| accepting a suitable type:
 | |
| 
 | |
| ```py
 | |
| class HasGetAttr:
 | |
|     def __getattr__(self, attr: str) -> int:
 | |
|         return 42
 | |
| 
 | |
| static_assert(is_subtype_of(HasGetAttr, HasXProperty))
 | |
| static_assert(is_assignable_to(HasGetAttr, HasXProperty))
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr))  # error: [static-assert-error]
 | |
| static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr))  # error: [static-assert-error]
 | |
| 
 | |
| class HasGetAttrWithUnsuitableReturn:
 | |
|     def __getattr__(self, attr: str) -> tuple[int, int]:
 | |
|         return (1, 2)
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(not is_subtype_of(HasGetAttrWithUnsuitableReturn, HasXProperty))  # error: [static-assert-error]
 | |
| static_assert(not is_assignable_to(HasGetAttrWithUnsuitableReturn, HasXProperty))  # error: [static-assert-error]
 | |
| 
 | |
| class HasGetAttrAndSetAttr:
 | |
|     def __getattr__(self, attr: str) -> MyInt:
 | |
|         return MyInt(0)
 | |
| 
 | |
|     def __setattr__(self, attr: str, value: int) -> None: ...
 | |
| 
 | |
| static_assert(is_subtype_of(HasGetAttrAndSetAttr, HasXProperty))
 | |
| static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty))
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(is_subtype_of(HasGetAttrAndSetAttr, XAsymmetricProperty))  # error: [static-assert-error]
 | |
| static_assert(is_assignable_to(HasGetAttrAndSetAttr, XAsymmetricProperty))  # error: [static-assert-error]
 | |
| ```
 | |
| 
 | |
| ## Subtyping of protocols with method members
 | |
| 
 | |
| A protocol can have method members. `T` is assignable to `P` in the following example because the
 | |
| class `T` has a method `m` which is assignable to the `Callable` supertype of the method `P.m`:
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol
 | |
| from ty_extensions import is_subtype_of, static_assert
 | |
| 
 | |
| class P(Protocol):
 | |
|     def m(self, x: int, /) -> None: ...
 | |
| 
 | |
| class NominalSubtype:
 | |
|     def m(self, y: int) -> None: ...
 | |
| 
 | |
| class NotSubtype:
 | |
|     def m(self, x: int) -> int:
 | |
|         return 42
 | |
| 
 | |
| static_assert(is_subtype_of(NominalSubtype, P))
 | |
| 
 | |
| # TODO: should pass
 | |
| static_assert(not is_subtype_of(NotSubtype, P))  # error: [static-assert-error]
 | |
| ```
 | |
| 
 | |
| A callable instance attribute is not sufficient for a type to satisfy a protocol with a method
 | |
| member: a method member specified by a protocol `P` must exist on the *meta-type* of `T` for `T` to
 | |
| be a subtype of `P`:
 | |
| 
 | |
| ```py
 | |
| from typing import Callable, Protocol
 | |
| from ty_extensions import static_assert, is_assignable_to
 | |
| 
 | |
| class SupportsFooMethod(Protocol):
 | |
|     def foo(self): ...
 | |
| 
 | |
| class SupportsFooAttr(Protocol):
 | |
|     foo: Callable[..., object]
 | |
| 
 | |
| class Foo:
 | |
|     def __init__(self):
 | |
|         self.foo: Callable[..., object] = lambda *args, **kwargs: None
 | |
| 
 | |
| static_assert(not is_assignable_to(Foo, SupportsFooMethod))
 | |
| static_assert(is_assignable_to(Foo, SupportsFooAttr))
 | |
| ```
 | |
| 
 | |
| The reason for this is that some methods, such as dunder methods, are always looked up on the class
 | |
| directly. If a class with an `__iter__` instance attribute satisfied the `Iterable` protocol, for
 | |
| example, the `Iterable` protocol would not accurately describe the requirements Python has for a
 | |
| class to be iterable at runtime. Allowing callable instance attributes to satisfy method members of
 | |
| protocols would also make `issubclass()` narrowing of runtime-checkable protocols unsound, as the
 | |
| `issubclass()` mechanism at runtime for protocols only checks whether a method is accessible on the
 | |
| class object, not the instance. (Protocols with non-method members cannot be passed to
 | |
| `issubclass()` at all at runtime.)
 | |
| 
 | |
| ```py
 | |
| from typing import Iterable, Any
 | |
| from ty_extensions import static_assert, is_assignable_to
 | |
| 
 | |
| class Foo:
 | |
|     def __init__(self):
 | |
|         self.__iter__: Callable[..., object] = lambda *args, **kwargs: None
 | |
| 
 | |
| static_assert(not is_assignable_to(Foo, Iterable[Any]))
 | |
| ```
 | |
| 
 | |
| Because method members must always be available on the class, it is safe to access a method on
 | |
| `type[P]`, where `P` is a protocol class, just like it is generally safe to access a method on
 | |
| `type[C]` where `C` is a nominal class:
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol
 | |
| 
 | |
| class Foo(Protocol):
 | |
|     def method(self) -> str: ...
 | |
| 
 | |
| def f(x: Foo):
 | |
|     reveal_type(type(x).method)  # revealed: def method(self) -> str
 | |
| 
 | |
| class Bar:
 | |
|     def __init__(self):
 | |
|         self.method = lambda: "foo"
 | |
| 
 | |
| f(Bar())  # error: [invalid-argument-type]
 | |
| ```
 | |
| 
 | |
| ## Equivalence of protocols with method members
 | |
| 
 | |
| Two protocols `P1` and `P2`, both with a method member `x`, are considered equivalent if the
 | |
| signature of `P1.x` is equivalent to the signature of `P2.x`, even though ty would normally model
 | |
| any two function definitions as inhabiting distinct function-literal types.
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol
 | |
| from ty_extensions import is_equivalent_to, static_assert
 | |
| 
 | |
| class P1(Protocol):
 | |
|     def x(self, y: int) -> None: ...
 | |
| 
 | |
| class P2(Protocol):
 | |
|     def x(self, y: int) -> None: ...
 | |
| 
 | |
| static_assert(is_equivalent_to(P1, P2))
 | |
| ```
 | |
| 
 | |
| As with protocols that only have non-method members, this also holds true when they appear in
 | |
| differently ordered unions:
 | |
| 
 | |
| ```py
 | |
| class A: ...
 | |
| class B: ...
 | |
| 
 | |
| static_assert(is_equivalent_to(A | B | P1, P2 | B | A))
 | |
| ```
 | |
| 
 | |
| ## Narrowing of protocols
 | |
| 
 | |
| <!-- snapshot-diagnostics -->
 | |
| 
 | |
| By default, a protocol class cannot be used as the second argument to `isinstance()` or
 | |
| `issubclass()`, and a type checker must emit an error on such calls. However, we still narrow the
 | |
| type inside these branches (this matches the behavior of other type checkers):
 | |
| 
 | |
| ```py
 | |
| from typing_extensions import Protocol, reveal_type
 | |
| 
 | |
| class HasX(Protocol):
 | |
|     x: int
 | |
| 
 | |
| def f(arg: object, arg2: type):
 | |
|     if isinstance(arg, HasX):  # error: [invalid-argument-type]
 | |
|         reveal_type(arg)  # revealed: HasX
 | |
|     else:
 | |
|         reveal_type(arg)  # revealed: ~HasX
 | |
| 
 | |
|     if issubclass(arg2, HasX):  # error: [invalid-argument-type]
 | |
|         reveal_type(arg2)  # revealed: type[HasX]
 | |
|     else:
 | |
|         reveal_type(arg2)  # revealed: type & ~type[HasX]
 | |
| ```
 | |
| 
 | |
| A protocol class decorated with `@typing(_extensions).runtime_checkable` *can* be used as the second
 | |
| argument to `isisinstance()` at runtime:
 | |
| 
 | |
| ```py
 | |
| from typing import runtime_checkable
 | |
| 
 | |
| @runtime_checkable
 | |
| class RuntimeCheckableHasX(Protocol):
 | |
|     x: int
 | |
| 
 | |
| def f(arg: object):
 | |
|     if isinstance(arg, RuntimeCheckableHasX):  # no error!
 | |
|         reveal_type(arg)  # revealed: RuntimeCheckableHasX
 | |
|     else:
 | |
|         reveal_type(arg)  # revealed: ~RuntimeCheckableHasX
 | |
| ```
 | |
| 
 | |
| but in order for a protocol class to be used as the second argument to `issubclass()`, it must
 | |
| satisfy two conditions:
 | |
| 
 | |
| 1. It must be decorated with `@runtime_checkable`
 | |
| 1. It must *only* have method members (protocols with attribute members are not permitted)
 | |
| 
 | |
| ```py
 | |
| @runtime_checkable
 | |
| class OnlyMethodMembers(Protocol):
 | |
|     def method(self) -> None: ...
 | |
| 
 | |
| def f(arg1: type, arg2: type):
 | |
|     if issubclass(arg1, RuntimeCheckableHasX):  # TODO: should emit an error here (has non-method members)
 | |
|         reveal_type(arg1)  # revealed: type[RuntimeCheckableHasX]
 | |
|     else:
 | |
|         reveal_type(arg1)  # revealed: type & ~type[RuntimeCheckableHasX]
 | |
| 
 | |
|     if issubclass(arg2, OnlyMethodMembers):  # no error!
 | |
|         reveal_type(arg2)  # revealed: type[OnlyMethodMembers]
 | |
|     else:
 | |
|         reveal_type(arg2)  # revealed: type & ~type[OnlyMethodMembers]
 | |
| ```
 | |
| 
 | |
| ## Truthiness of protocol instance
 | |
| 
 | |
| An instance of a protocol type generally has ambiguous truthiness:
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol
 | |
| 
 | |
| class Foo(Protocol):
 | |
|     x: int
 | |
| 
 | |
| def f(foo: Foo):
 | |
|     reveal_type(bool(foo))  # revealed: bool
 | |
| ```
 | |
| 
 | |
| But this is not the case if the protocol has a `__bool__` method member that returns `Literal[True]`
 | |
| or `Literal[False]`:
 | |
| 
 | |
| ```py
 | |
| from typing import Literal
 | |
| 
 | |
| class Truthy(Protocol):
 | |
|     def __bool__(self) -> Literal[True]: ...
 | |
| 
 | |
| class FalsyFoo(Foo, Protocol):
 | |
|     def __bool__(self) -> Literal[False]: ...
 | |
| 
 | |
| class FalsyFooSubclass(FalsyFoo, Protocol):
 | |
|     y: str
 | |
| 
 | |
| def g(a: Truthy, b: FalsyFoo, c: FalsyFooSubclass):
 | |
|     reveal_type(bool(a))  # revealed: Literal[True]
 | |
|     reveal_type(bool(b))  # revealed: Literal[False]
 | |
|     reveal_type(bool(c))  # revealed: Literal[False]
 | |
| ```
 | |
| 
 | |
| The same works with a class-level declaration of `__bool__`:
 | |
| 
 | |
| ```py
 | |
| from typing import Callable
 | |
| 
 | |
| class InstanceAttrBool(Protocol):
 | |
|     __bool__: Callable[[], Literal[True]]
 | |
| 
 | |
| def h(obj: InstanceAttrBool):
 | |
|     reveal_type(bool(obj))  # revealed: Literal[True]
 | |
| ```
 | |
| 
 | |
| ## Callable protocols
 | |
| 
 | |
| An instance of a protocol type is callable if the protocol defines a `__call__` method:
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol
 | |
| 
 | |
| class CallMeMaybe(Protocol):
 | |
|     def __call__(self, x: int) -> str: ...
 | |
| 
 | |
| def f(obj: CallMeMaybe):
 | |
|     reveal_type(obj(42))  # revealed: str
 | |
|     obj("bar")  # error: [invalid-argument-type]
 | |
| ```
 | |
| 
 | |
| An instance of a protocol like this can be assignable to a `Callable` type, but only if it has the
 | |
| right signature:
 | |
| 
 | |
| ```py
 | |
| from typing import Callable
 | |
| from ty_extensions import is_subtype_of, is_assignable_to, static_assert
 | |
| 
 | |
| static_assert(is_subtype_of(CallMeMaybe, Callable[[int], str]))
 | |
| static_assert(is_assignable_to(CallMeMaybe, Callable[[int], str]))
 | |
| static_assert(not is_subtype_of(CallMeMaybe, Callable[[str], str]))
 | |
| static_assert(not is_assignable_to(CallMeMaybe, Callable[[str], str]))
 | |
| static_assert(not is_subtype_of(CallMeMaybe, Callable[[CallMeMaybe, int], str]))
 | |
| static_assert(not is_assignable_to(CallMeMaybe, Callable[[CallMeMaybe, int], str]))
 | |
| 
 | |
| def g(obj: Callable[[int], str], obj2: CallMeMaybe, obj3: Callable[[str], str]):
 | |
|     obj = obj2
 | |
|     obj3 = obj2  # error: [invalid-assignment]
 | |
| ```
 | |
| 
 | |
| By the same token, a `Callable` type can also be assignable to a protocol-instance type if the
 | |
| signature implied by the `Callable` type is assignable to the signature of the `__call__` method
 | |
| specified by the protocol:
 | |
| 
 | |
| ```py
 | |
| from ty_extensions import TypeOf
 | |
| 
 | |
| class Foo(Protocol):
 | |
|     def __call__(self, x: int, /) -> str: ...
 | |
| 
 | |
| static_assert(is_subtype_of(Callable[[int], str], Foo))
 | |
| static_assert(is_assignable_to(Callable[[int], str], Foo))
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(not is_subtype_of(Callable[[str], str], Foo))  # error: [static-assert-error]
 | |
| static_assert(not is_assignable_to(Callable[[str], str], Foo))  # error: [static-assert-error]
 | |
| static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo))  # error: [static-assert-error]
 | |
| static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo))  # error: [static-assert-error]
 | |
| 
 | |
| def h(obj: Callable[[int], str], obj2: Foo, obj3: Callable[[str], str]):
 | |
|     obj2 = obj
 | |
| 
 | |
|     # TODO: we should emit [invalid-assignment] here because the signature of `obj3` is not assignable
 | |
|     # to the declared type of `obj2`
 | |
|     obj2 = obj3
 | |
| 
 | |
| def satisfies_foo(x: int) -> str:
 | |
|     return "foo"
 | |
| 
 | |
| static_assert(is_subtype_of(TypeOf[satisfies_foo], Foo))
 | |
| static_assert(is_assignable_to(TypeOf[satisfies_foo], Foo))
 | |
| ```
 | |
| 
 | |
| ## Protocols are never singleton types, and are never single-valued types
 | |
| 
 | |
| It *might* be possible to have a singleton protocol-instance type...?
 | |
| 
 | |
| For example, `WeirdAndWacky` in the following snippet only has a single possible inhabitant: `None`!
 | |
| It is thus a singleton type. However, going out of our way to recognize it as such is probably not
 | |
| worth it. Such cases should anyway be exceedingly rare and/or contrived.
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol, Callable
 | |
| from ty_extensions import is_singleton, is_single_valued
 | |
| 
 | |
| class WeirdAndWacky(Protocol):
 | |
|     @property
 | |
|     def __class__(self) -> Callable[[], None]: ...
 | |
| 
 | |
| reveal_type(is_singleton(WeirdAndWacky))  # revealed: Literal[False]
 | |
| reveal_type(is_single_valued(WeirdAndWacky))  # revealed: Literal[False]
 | |
| ```
 | |
| 
 | |
| ## Integration test: `typing.SupportsIndex` and `typing.Sized`
 | |
| 
 | |
| `typing.SupportsIndex` and `typing.Sized` are two protocols that are very commonly used in the wild.
 | |
| 
 | |
| ```py
 | |
| from typing import SupportsIndex, Sized, Literal
 | |
| 
 | |
| def one(some_int: int, some_literal_int: Literal[1], some_indexable: SupportsIndex):
 | |
|     a: SupportsIndex = some_int
 | |
|     b: SupportsIndex = some_literal_int
 | |
|     c: SupportsIndex = some_indexable
 | |
| 
 | |
| def two(some_list: list, some_tuple: tuple[int, str], some_sized: Sized):
 | |
|     a: Sized = some_list
 | |
|     b: Sized = some_tuple
 | |
|     c: Sized = some_sized
 | |
| ```
 | |
| 
 | |
| ## Recursive protocols
 | |
| 
 | |
| ### Properties
 | |
| 
 | |
| ```py
 | |
| from __future__ import annotations
 | |
| 
 | |
| from typing import Protocol, Any, TypeVar
 | |
| from ty_extensions import static_assert, is_assignable_to, is_subtype_of, is_equivalent_to
 | |
| 
 | |
| class RecursiveFullyStatic(Protocol):
 | |
|     parent: RecursiveFullyStatic
 | |
|     x: int
 | |
| 
 | |
| class RecursiveNonFullyStatic(Protocol):
 | |
|     parent: RecursiveNonFullyStatic
 | |
|     x: Any
 | |
| 
 | |
| # TODO: these should pass, once we take into account types of members
 | |
| static_assert(not is_subtype_of(RecursiveFullyStatic, RecursiveNonFullyStatic))  # error: [static-assert-error]
 | |
| static_assert(not is_subtype_of(RecursiveNonFullyStatic, RecursiveFullyStatic))  # error: [static-assert-error]
 | |
| 
 | |
| static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveNonFullyStatic))
 | |
| static_assert(is_assignable_to(RecursiveFullyStatic, RecursiveNonFullyStatic))
 | |
| static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveFullyStatic))
 | |
| 
 | |
| class AlsoRecursiveFullyStatic(Protocol):
 | |
|     parent: AlsoRecursiveFullyStatic
 | |
|     x: int
 | |
| 
 | |
| static_assert(is_equivalent_to(AlsoRecursiveFullyStatic, RecursiveFullyStatic))
 | |
| 
 | |
| class RecursiveOptionalParent(Protocol):
 | |
|     parent: RecursiveOptionalParent | None
 | |
| 
 | |
| static_assert(is_assignable_to(RecursiveOptionalParent, RecursiveOptionalParent))
 | |
| 
 | |
| static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveOptionalParent))
 | |
| static_assert(not is_assignable_to(RecursiveOptionalParent, RecursiveNonFullyStatic))
 | |
| 
 | |
| class Other(Protocol):
 | |
|     z: str
 | |
| 
 | |
| def _(rec: RecursiveFullyStatic, other: Other):
 | |
|     reveal_type(rec.parent.parent.parent)  # revealed: RecursiveFullyStatic
 | |
| 
 | |
|     rec.parent.parent.parent = rec
 | |
|     rec = rec.parent.parent.parent
 | |
| 
 | |
|     rec.parent.parent.parent = other  # error: [invalid-assignment]
 | |
|     other = rec.parent.parent.parent  # error: [invalid-assignment]
 | |
| 
 | |
| class Foo(Protocol):
 | |
|     @property
 | |
|     def x(self) -> "Foo": ...
 | |
| 
 | |
| class Bar(Protocol):
 | |
|     @property
 | |
|     def x(self) -> "Bar": ...
 | |
| 
 | |
| # TODO: this should pass
 | |
| # error: [static-assert-error]
 | |
| static_assert(is_equivalent_to(Foo, Bar))
 | |
| 
 | |
| T = TypeVar("T", bound="TypeVarRecursive")
 | |
| 
 | |
| class TypeVarRecursive(Protocol):
 | |
|     # TODO: commenting this out will cause a stack overflow.
 | |
|     # x: T
 | |
|     y: "TypeVarRecursive"
 | |
| 
 | |
| def _(t: TypeVarRecursive):
 | |
|     # reveal_type(t.x)  # revealed: T
 | |
|     reveal_type(t.y)  # revealed: TypeVarRecursive
 | |
| ```
 | |
| 
 | |
| ### Nested occurrences of self-reference
 | |
| 
 | |
| Make sure that we handle self-reference correctly, even if the self-reference appears deeply nested
 | |
| within the type of a protocol member:
 | |
| 
 | |
| ```toml
 | |
| [environment]
 | |
| python-version = "3.12"
 | |
| ```
 | |
| 
 | |
| ```py
 | |
| from __future__ import annotations
 | |
| 
 | |
| from typing import Protocol, Callable
 | |
| from ty_extensions import Intersection, Not, is_assignable_to, is_equivalent_to, static_assert
 | |
| 
 | |
| class C: ...
 | |
| 
 | |
| class GenericC[T](Protocol):
 | |
|     pass
 | |
| 
 | |
| class Recursive(Protocol):
 | |
|     direct: Recursive
 | |
| 
 | |
|     union: None | Recursive
 | |
| 
 | |
|     intersection1: Intersection[C, Recursive]
 | |
|     intersection2: Intersection[C, Not[Recursive]]
 | |
| 
 | |
|     t: tuple[int, tuple[str, Recursive]]
 | |
| 
 | |
|     callable1: Callable[[int], Recursive]
 | |
|     callable2: Callable[[Recursive], int]
 | |
| 
 | |
|     subtype_of: type[Recursive]
 | |
| 
 | |
|     generic: GenericC[Recursive]
 | |
| 
 | |
|     def method(self, x: Recursive) -> Recursive: ...
 | |
| 
 | |
|     nested: Recursive | Callable[[Recursive | Recursive, tuple[Recursive, Recursive]], Recursive | Recursive]
 | |
| 
 | |
| static_assert(is_equivalent_to(Recursive, Recursive))
 | |
| static_assert(is_assignable_to(Recursive, Recursive))
 | |
| 
 | |
| def _(r: Recursive):
 | |
|     reveal_type(r.direct)  # revealed: Recursive
 | |
|     reveal_type(r.union)  # revealed: None | Recursive
 | |
|     reveal_type(r.intersection1)  # revealed: C & Recursive
 | |
|     reveal_type(r.intersection2)  # revealed: C
 | |
|     reveal_type(r.t)  # revealed: tuple[int, tuple[str, Recursive]]
 | |
|     reveal_type(r.callable1)  # revealed: (int, /) -> Recursive
 | |
|     reveal_type(r.callable2)  # revealed: (Recursive, /) -> int
 | |
|     reveal_type(r.subtype_of)  # revealed: @Todo(type[T] for protocols)
 | |
|     reveal_type(r.generic)  # revealed: GenericC[Recursive]
 | |
|     reveal_type(r.method(r))  # revealed: Recursive
 | |
|     reveal_type(r.nested)  # revealed: Recursive | ((Recursive, tuple[Recursive, Recursive], /) -> Recursive)
 | |
| 
 | |
|     reveal_type(r.method(r).callable1(1).direct.t[1][1])  # revealed: Recursive
 | |
| ```
 | |
| 
 | |
| ### Mutually-recursive protocols
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol
 | |
| from ty_extensions import is_equivalent_to, static_assert
 | |
| 
 | |
| class Foo(Protocol):
 | |
|     x: "Bar"
 | |
| 
 | |
| class Bar(Protocol):
 | |
|     x: Foo
 | |
| 
 | |
| static_assert(is_equivalent_to(Foo, Bar))
 | |
| ```
 | |
| 
 | |
| ### Disjointness of recursive protocol and recursive final type
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol
 | |
| from ty_extensions import is_disjoint_from, static_assert
 | |
| 
 | |
| class Proto(Protocol):
 | |
|     x: "Proto"
 | |
| 
 | |
| class Nominal:
 | |
|     x: "Nominal"
 | |
| 
 | |
| static_assert(not is_disjoint_from(Proto, Nominal))
 | |
| ```
 | |
| 
 | |
| ### Regression test: narrowing with self-referential protocols
 | |
| 
 | |
| This snippet caused us to panic on an early version of the implementation for protocols.
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol
 | |
| 
 | |
| class A(Protocol):
 | |
|     def x(self) -> "B | A": ...
 | |
| 
 | |
| class B(Protocol):
 | |
|     def y(self): ...
 | |
| 
 | |
| obj = something_unresolvable  # error: [unresolved-reference]
 | |
| reveal_type(obj)  # revealed: Unknown
 | |
| if isinstance(obj, (B, A)):
 | |
|     reveal_type(obj)  # revealed: (Unknown & B) | (Unknown & A)
 | |
| ```
 | |
| 
 | |
| ### Protocols that use `Self`
 | |
| 
 | |
| `Self` is a `TypeVar` with an upper bound of the class in which it is defined. This means that
 | |
| `Self` annotations in protocols can also be tricky to handle without infinite recursion and stack
 | |
| overflows.
 | |
| 
 | |
| ```toml
 | |
| [environment]
 | |
| python-version = "3.12"
 | |
| ```
 | |
| 
 | |
| ```py
 | |
| from typing_extensions import Protocol, Self
 | |
| from ty_extensions import static_assert
 | |
| 
 | |
| class _HashObject(Protocol):
 | |
|     def copy(self) -> Self: ...
 | |
| 
 | |
| class Foo: ...
 | |
| 
 | |
| # Attempting to build this union caused us to overflow on an early version of
 | |
| # <https://github.com/astral-sh/ruff/pull/18659>
 | |
| x: Foo | _HashObject
 | |
| ```
 | |
| 
 | |
| Some other similar cases that caused issues in our early `Protocol` implementation:
 | |
| 
 | |
| `a.py`:
 | |
| 
 | |
| ```py
 | |
| from typing_extensions import Protocol, Self
 | |
| 
 | |
| class PGconn(Protocol):
 | |
|     def connect(self) -> Self: ...
 | |
| 
 | |
| class Connection:
 | |
|     pgconn: PGconn
 | |
| 
 | |
| def is_crdb(conn: PGconn) -> bool:
 | |
|     return isinstance(conn, Connection)
 | |
| ```
 | |
| 
 | |
| and:
 | |
| 
 | |
| `b.py`:
 | |
| 
 | |
| ```py
 | |
| from typing_extensions import Protocol
 | |
| 
 | |
| class PGconn(Protocol):
 | |
|     def connect[T: PGconn](self: T) -> T: ...
 | |
| 
 | |
| class Connection:
 | |
|     pgconn: PGconn
 | |
| 
 | |
| def f(x: PGconn):
 | |
|     isinstance(x, Connection)
 | |
| ```
 | |
| 
 | |
| ### Recursive protocols used as the first argument to `cast()`
 | |
| 
 | |
| These caused issues in an early version of our `Protocol` implementation due to the fact that we use
 | |
| a recursive function in our `cast()` implementation to check whether a type contains `Unknown` or
 | |
| `Todo`. Recklessly recursing into a type causes stack overflows if the type is recursive:
 | |
| 
 | |
| ```toml
 | |
| [environment]
 | |
| python-version = "3.12"
 | |
| ```
 | |
| 
 | |
| ```py
 | |
| from __future__ import annotations
 | |
| from typing import cast, Protocol
 | |
| 
 | |
| class Iterator[T](Protocol):
 | |
|     def __iter__(self) -> Iterator[T]: ...
 | |
| 
 | |
| def f(value: Iterator):
 | |
|     cast(Iterator, value)  # error: [redundant-cast]
 | |
| ```
 | |
| 
 | |
| ## Meta-protocols
 | |
| 
 | |
| Where `P` is a protocol type, a class object `N` can be said to inhabit the type `type[P]` if:
 | |
| 
 | |
| - All `ClassVar` members on `P` exist on the class object `N`
 | |
| - All method members on `P` exist on the class object `N`
 | |
| - Instantiating `N` creates an object that would satisfy the protocol `P`
 | |
| 
 | |
| Currently meta-protocols are not fully supported by ty, but we try to keep false positives to a
 | |
| minimum in the meantime.
 | |
| 
 | |
| ```py
 | |
| from typing import Protocol, ClassVar
 | |
| from ty_extensions import static_assert, is_assignable_to, TypeOf, is_subtype_of
 | |
| 
 | |
| class Foo(Protocol):
 | |
|     x: int
 | |
|     y: ClassVar[str]
 | |
|     def method(self) -> bytes: ...
 | |
| 
 | |
| def _(f: type[Foo]):
 | |
|     reveal_type(f)  # revealed: type[@Todo(type[T] for protocols)]
 | |
| 
 | |
|     # TODO: we should emit `unresolved-attribute` here: although we would accept this for a
 | |
|     # nominal class, we would see any class `N` as inhabiting `Foo` if it had an implicit
 | |
|     # instance attribute `x`, and implicit instance attributes are rarely bound on the class
 | |
|     # object.
 | |
|     reveal_type(f.x)  # revealed: @Todo(type[T] for protocols)
 | |
| 
 | |
|     # TODO: should be `str`
 | |
|     reveal_type(f.y)  # revealed: @Todo(type[T] for protocols)
 | |
|     f.y = "foo"  # fine
 | |
| 
 | |
|     # TODO: should be `Callable[[Foo], bytes]`
 | |
|     reveal_type(f.method)  # revealed: @Todo(type[T] for protocols)
 | |
| 
 | |
| class Bar: ...
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(not is_assignable_to(type[Bar], type[Foo]))  # error: [static-assert-error]
 | |
| static_assert(not is_assignable_to(TypeOf[Bar], type[Foo]))  # error: [static-assert-error]
 | |
| 
 | |
| class Baz:
 | |
|     x: int
 | |
|     y: ClassVar[str] = "foo"
 | |
|     def method(self) -> bytes:
 | |
|         return b"foo"
 | |
| 
 | |
| static_assert(is_assignable_to(type[Baz], type[Foo]))
 | |
| static_assert(is_assignable_to(TypeOf[Baz], type[Foo]))
 | |
| 
 | |
| # TODO: these should pass
 | |
| static_assert(is_subtype_of(type[Baz], type[Foo]))  # error: [static-assert-error]
 | |
| static_assert(is_subtype_of(TypeOf[Baz], type[Foo]))  # error: [static-assert-error]
 | |
| ```
 | |
| 
 | |
| ## Regression test for `ClassVar` members in stubs
 | |
| 
 | |
| In an early version of our protocol implementation, we didn't retain the `ClassVar` qualifier for
 | |
| protocols defined in stub files.
 | |
| 
 | |
| `stub.pyi`:
 | |
| 
 | |
| ```pyi
 | |
| from typing import ClassVar, Protocol
 | |
| 
 | |
| class Foo(Protocol):
 | |
|     x: ClassVar[int]
 | |
| ```
 | |
| 
 | |
| `main.py`:
 | |
| 
 | |
| ```py
 | |
| from stub import Foo
 | |
| from ty_extensions import reveal_protocol_interface
 | |
| 
 | |
| # error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`; ClassVar)}`"
 | |
| reveal_protocol_interface(Foo)
 | |
| ```
 | |
| 
 | |
| ## TODO
 | |
| 
 | |
| Add tests for:
 | |
| 
 | |
| - More tests for protocols inside `type[]`. [Spec reference][protocols_inside_type_spec].
 | |
| - Protocols with instance-method members, including:
 | |
|     - Protocols with methods that have parameters or the return type unannotated
 | |
|     - Protocols with methods that have parameters or the return type annotated with `Any`
 | |
| - Protocols with `@classmethod` and `@staticmethod`
 | |
| - Assignability of non-instance types to protocols with instance-method members (e.g. a
 | |
|     class-literal type can be a subtype of `Sized` if its metaclass has a `__len__` method)
 | |
| - Protocols with methods that have annotated `self` parameters.
 | |
|     [Spec reference][self_types_protocols_spec].
 | |
| - Protocols with overloaded method members
 | |
| - `super()` on nominal subtypes (explicit and implicit) of protocol classes
 | |
| - [Recursive protocols][recursive_protocols_spec]
 | |
| - Generic protocols
 | |
| - Non-generic protocols with function-scoped generic methods
 | |
| - Protocols with instance attributes annotated with `Callable` (can a nominal type with a method
 | |
|     satisfy that protocol, and if so in what cases?)
 | |
| - Protocols decorated with `@final`
 | |
| - Equivalence and subtyping between `Callable` types and protocols that define `__call__`
 | |
| 
 | |
| [mypy_protocol_docs]: https://mypy.readthedocs.io/en/stable/protocols.html#protocols-and-structural-subtyping
 | |
| [mypy_protocol_tests]: https://github.com/python/mypy/blob/master/test-data/unit/check-protocols.test
 | |
| [protocol conformance tests]: https://github.com/python/typing/tree/main/conformance/tests
 | |
| [protocols_inside_type_spec]: https://typing.python.org/en/latest/spec/protocol.html#type-and-class-objects-vs-protocols
 | |
| [recursive_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#recursive-protocols
 | |
| [self_types_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#self-types-in-protocols
 | |
| [spec_protocol_members]: https://typing.python.org/en/latest/spec/protocol.html#protocol-members
 | |
| [typing_spec_protocols]: https://typing.python.org/en/latest/spec/protocol.html
 | 
