Hi! I read through the draft PEP and had a couple questions about the specification:
(1) All of the examples of type-checked metadata show an instance of a class with a __supports_type__ method, but the specification as written would also allow things like this:
(SpecialString is an object with a __supports_type__ method, and SpecialString.__supports_type__("") type-checks without error.) Is this intentionally allowed?
(2) What happens for a metadata object that has a __supports_type__ attribute that doesn’t fit the requirements? E.g., the attribute isn’t a method, or it is a method but has the wrong return type?
I’ve implemented provisional support for PEP 746 in pyright. This will give us a chance to play with the concept before the spec is cast in concrete. This functionality will be included in next week’s release of pyright (1.1.367). It is currently disabled by default but can be enabled by setting enableExperimentalFeatures to “true” in your pyright config file.
Here is the commit if you’re interested in the implementation details or test cases. Compared to most typing PEPs, this was very simple to implement.
As for the questions @rchen152 posed, I interpreted the wording in the spec to mean that the __supports_type__ attribute should be treated like most other magic methods in Python. If the object has an attribute named __supports_type__, the type checker binds it to its object and then simulates a “call” with a single positional argument. If that “call” fails because the object isn’t callable or the argument is incompatible with the callable’s signature, then the metadata type is considered incompatible.
If the “call” succeeds, then its return type is examined. If that type is statically determined to be falsy under all situations (e.g. Literal[False], None, Literal[0]), then the metadata type is considered incompatible. If the return type is possibly truthy (e.g. bool, int or Any), then the metadata type is considered compatible.
The current draft of the spec indicates that __supports_type__ should return a bool value. In my opinion, it’s fine for the spec to leave the behavior undefined in cases where the return type is not bool, but I’m not opposed to clarifying the behavior in cases where it’s not a bool.
Yes, I don’t see why not. The spec specifies a protocol (which is now given in code form) that this example would conform to, so it is allowed by the spec.
Amazing Eric, thank you so much! I’m glad it wasn’t too hard to implement.
I’ll add support to Pydantic and annotated-types in the next couple of days. I’ll try to also make a PR to msgspec but I’m not sure the timeline for that.
Sure, I’m not opposed to it. I’m coming at this from the angle that because there are multiple type checkers that we don’t want to implement things in incompatible ways, peps should try to be as complete and unambiguous as possible. Hence the questions about supported cases and edge cases.
I suggested a small update to the draft based on what you and Eric described. Let me know what you think =)
Edit: forgot to comment on the proposed names. For what it’s worth, I don’t mind __supports_type__, but of the other proposals, __accepts_type__, __validate_value__, and __check_value__ all seem decent to me.
Thanks. I feel the _type suffix is slightly off if we don’t actually pass a type to the method. Therefore I’d favor __is_applicable__, which I think fits nicely with the purpose of the PEP: we ask whether a piece of metadata is applicable to a given type.
I like __is_applicable__. But don’t you think it should reference metadata or annotation? Otherwise it seems a bit generic of a name, just browsing code I don’t think I’d be able to guess what it does. __is_metadata_applicable__?
That feels wrong since we’re asking the metadata whether the “base” type is applicable, not the other way around.
Aside: I feel we could use some better terminology around Annotated. There is no term for the first argument (I’d like to call it the base type) and the remaining arguments are called “annotations”, which is a really confusing term because we also have annotations in the Python syntax. We should call them “metadata” instead.
So I’d like the terminology to be:
Annotated[BaseType, Metadata1, Metadata2, ...]
If others agree with this terminology I can add it to the typing spec.
Agreed better terminology is needed. That sounds good to me.
Yeah, that makes sense. I guess __is_applicable__ is clear enough if you already know the class is meant to be Annotated metadata (and I generally discourage folks from re-using types as metadata so it should be obvious it’s metadata). And it will mainly be inside of libraries anyway where discoverability is less important than it is for something exposed to end-users. If you agree I can update the PEP tomorrow.
@adriangb, I agree with your previous observation that __is_applicable__ sounds too generic.
Just throwing out a few additional ideas: __metacheck__ (similar to __instancecheck__) __check_metadata__ __metadata_check__ __apply_metadata__ __apply_meta__ __metadata__ __annotated_metadata__ __apply_annotated__
None of these strike me as great options, but I prefer most of them to __is_applicable__.
@Jelle, I like the idea of improving the terminology. The term “metadata” sounds good to me, but I have some misgivings about “base type”. Given the flexibility of Annotated, there’s no guarantee that the metadata applies to the type. Maybe we should avoid any qualifiers and simply refer to it as the “type”?
I think for this you should use a bounded type variable. Your example should be an error because T is not compatible with Gt(0)'s argument; it’s as if you had written def f[T](x: T): return x > 0.
Instead, you should write something like type Positive[T: SupportsGt[int]] = Annotated[T, Gt(0)].
That makes sense. While it’s somewhat verbose I expect use of aliases like this to be either libraries or advanced enough use cases that if users want it to work with the type system the verbosity is acceptable.
Personally I think the name __supports_type__ might be too vague and can be interpreted in more contexts than one might desire. May I suggest something like __annotatable_on__
or __annotatable_with__?
I think some form of the word “annotation” should exist to immediately give the reader the context of annotations.
I don’t particularly love that Annotated is called Annotated but I agree that there is value in including annotated or annotation in the name to reference back to Annotated.