A Protocol with @runtime_checkable is classified as either Data or non-Data Protocol, with the former not being allowed for use in issubclass checks. A Data Protocol is any Protocol that “contains at least one non-method member” as per the typing spec. There is some ambiguity with this formulation (see a recent PyRight issue of mine):
Is this a strict definition, in that literally any non-method member of the Protocol makes it a Data Protocol? Or are there certain things that are technically non-method members of the Protocol itself, but don’t count for the classification?
I am specifically wondering about __slots__ = () here. While it itself is a non-method member (it is after all a regular attribute) the meaning is that there are no attributes. Should this be considered a Data Protocol or not?
from typing import Protocol, runtime_checkable
@runtime_checkable
class DataOrNonData(Protocol):
__slots__ = ()
def just_some_method(self) -> int: ...
PyRight (v1.1.406, possibly since v1.1.345) considers this a Data Protocol and rejects its use in issubclass. MyPy (v1.18.2) accepts its use in issubclass.
typeshed uses this pattern for typing.SupportsXYZ and contextlib.Abstract(Async)ContextManager.
I think that should be considered a bug, and that the spec should be updated here. It makes very little sense to allow (any?) dunder attributes to modify Protocol meanings in such a way, but mainly basics like __dict__, __slots__, __name__, __qualname__, __mro__ and similar.
Same class of bug as many others, this is a case where neither the spec nor a specific type checker is accounting for the data model. This shouldnt be a data protocol, and rather than carve out data model exceptions everywhere possibly relevent, the specification should have a section about names whose behavior and purpose are dictated by the data model.
Looks like people agree that __slots__ = () does not make a Data Protocol. Is it fine to just propose explicitly excluding that case in the spec?
I see that this could be generalised to all/most dunder attributes, but they all have slightly different semantics – a blanket statement probably isn’t appropriate. Does anyone consider it feasible to resolve the general case in a timely manner?
Personally, I of course have a self-interest to push for the quickest solution – explicitly excluding __slots__ = () – because otherwise a major type checker is unusable for me. But I don’t want to let this get in the way of fixing the spec properly.