Removal of the mentions of descriptors from the PEP means that the combination of explicit protocol implementation (via inheritance) and implementing a ReadOnly member via property won’t be possible.
It’s sad to have to compromise, but I’d rather do that than wait for yet another specification. Hopefully the eventual clarification to instance & class variables will enable what I want.
Imo the current interaction of type and Protocol attributes makes little sense.
Given:
class HasName(Protocol):
name: str
I think the following shouldn’t reveal anything but unknown. [1]
I don’t really know what type[HasName] is supposed to convey. For most intents and purposes, I think it’s about as wide as just using plain type, since the instance variable speaks nothing of the type object.
It could be str, it could not exist, or if it was ReadOnly in the protocol, it could be a property (or other descriptor).
If the intent is to use the type as a factory, one is likely better off using Callable[<params>, HasName].
If the type itself is expected to have an attribute name, it should be assignable to HasName anyway.
or at best the equivalent of T | Unbound, like pyright reports for loop variables outside the loop, which wouldn’t be very useful either ↩︎
Earlier version of this PEP allowed overriding descriptors;
this was removed since the current rules regarding type would make this unsafe.
We expect a future clarification of type to reintroduce this ability.
I agree. I think that type[P] where P is a protocol doesn’t have a coherent meaning as a type at all, and it is contrary to the existing language of the typing spec for a type checker to allow it.
The spec says:
Sometimes you want to talk about class objects, in particular class objects that inherit from a given class. This can be spelled as type[C] where C is a class.
A protocol is not a class (even though the class syntactic form is used to describe it.) It is a structural type which describes an interface, and any object implementing that interface inhabits the type.
Nothing can reasonably be known about the type of the inhabitants of a protocol, without making unwarranted assumptions about which objects inhabit the protocol. For example, the protocol HasName could be inhabited both by an instance of some class with a name attribute that is a str, but also by a class object with a class attribute name that is a str. There is nothing we can say with confidence about the type[] of all inhabitants of that protocol. It is wrong to assume that type[HasName] even has a name attribute; the type of the former inhabitant may not, the type of the latter (most likely just type) almost certainly doesn’t.
It does seem like it would be ideal to clarify both this, and some things about the behavior of descriptors, in the existing spec before adding the ReadOnly feature.
assignment to name on the type object must be compatible with the type-instance relationship as it exists in python’s runtime. This should mean that assigning a descriptor object is valid.
Access from the type can result in an attribute error, as it may not be bound to the type
Access from the type can result in a descriptor object which when otherwise accessed from an instance, would have resulted in a str
Without changing the rules of type, if the protocol describes it’s constructability (either via new or init) we should currently be able to assume that it’s constructable as the protocol says it is, resulting in an instance which has an accessible name field.
This is why I think the things I mentioned above here:
are necessary to properly describe the bounds on what we know about the type object.
No, I think that is reading far beyond what the protocol type expresses. An inhabitant of the protocol need not be an instance of some type to which such a descriptor can be assigned. There are many other ways in Python for an object to have an attribute.
One of the two examples I gave (a class with a class attribute name of type str, which both mypy and pyright – rightly – accept as an inhabitant of HasName) is of type type, and at runtime you cannot assign a descriptor to the name attribute of type.
Access of any attribute on any object can result in almost anything. What’s relevant when we are talking about a type is what we can say for sure about all inhabitants of that type. If the attribute may not even exist on some inhabitants of a type, then the only sound answer is for type checkers to say that that attribute does not exist on that type.
I think that type[P] is quite simple, and doesn’t require any consideration of descriptor complexities at all. Either it should not be a valid type at all (this is what I believe the current spec actually says), or it should be equivalent to type[object].
I’m aware, I did not say exclusively that a descriptor was the only assignable thing.
This isn’t quire right either, as assigning an int to name on the type would clearly be a violation, but assinging a str to it might possibly be fine.
I believe it should be a valid type, but expressing what we actually know about the type is far out of scope for this pep, and not something we should predicate this pep on. Whether or not people think it’s worth actually adding that complexity into the type system is another question beyond this, but the bound of what we know for a type[HasName] is different from type[object]
It’s a “possibly present” class attribute or descriptor, further bound by the presence or not of slots, and the presence or not of a mutable dict declared in those slots.
How much of that may be unknown from just a protocol, means that access is no less unsafe than access to a typed dict with NotRequired keys, and that we do have a bound on possible types of things when access is successful.
Rules for assignment without creating problems would require further evaluation, for all of the same possible unknowns, and could require some amount of narrowing details about the type object in advance, depending on how detailed the protocol was, and how correct the type system requires things be here (how much is accurately encoded into the type system)
I have had usecases in the past for this exact thing in the context of a registry where instances where looked up based on being a subclass of something, for example a runtime checkable protocol. I.e. we for sure know one operation that is valid, and that is using it on the right side of an isinstance check, which makes it have noticeable different behavior from type[object]. I agree that we don’t know anything extra about the attributes of type[P] compared to type[object], but I would be opposed to making the type in general illegal. (for very concrete examples of such a registry, look at Entity Component Systems. Not what I wanted it for, but quite similar)
I don’t want to sound too negative here, but it seems like this is something that’s never going to be accurately expressed in the type system, and the rest of your argument for type[Proto] meaning more than type[object] mostly depends on it being so, and getting the rules correct. I’d rather a type checker flag pretty much everything here, and ignore what I know is correct until then.