I don’t think the distinction between the two matters much from a reader’s point of view, as both special forms and annotations are conceptually modifiers to the declared type in some ways.
That special forms are mostly meant for static type checkers while annotations are mostly meant for runtime libraries, and that the two implement the @ operation differently and transform into different backwards-compatible constructs, are details that do not hamper a reader’s ability to grasp the semantics of the @-tags.
I cannot fathom how the search didn’t return An alternative to Annotated - #31 by loic-simon before i made this original post, but now it’s showing it for me in “New & Unread Topics”. basically the same idea going back further.
i very much think it needs to be the latter in order to feel like an actual coherent feature (particularly if generalized for Required/ReadOnly etc).
Yeah I thought there’s where you drew your first considered option from in the OP. Although the possibility has been talked about before, I think what’s new here is that style-wise recommending that there be no space between the @ operator and a tag name makes it read more intuitively as tags. That and the generalization to special forms.
Yeah and there’s precedent in the implementation of type.__or__ and _SpecialForm.__or__ for allowing A | B to represent Union[A, B].
I would strongly recommend against including Optional in this list. All others you listed clearly apply to names, not to types. But Optional[A] = A | None, a type level operation. This is to make a clear distinction between type operations and tags, with the latter applying to names, not types.
I disagree. This is exactly the confusion that was already talked about.
IMO a tag should make sense even if there is no type on the other side. This is true for all other tags you listed, even if they are a bit weird in their implications. But Optional is different: It only makes sense in combination with a type, since Any | None == Any.
A consequence of this argumentation is that these kinds of tags shouldn’t be valid inside of type aliases.
Yes, this is a purity argument. But IMO a purity argument is important to not corner ourselves in the future: What if we want to add tags that apply to a type alias, but don’t travel with the type? Or similar constructs?
IMO | None reads better and is clearer in intent. (I would even go so far as to say Optional was a mistake and should be deprecated).
It’s not even a special form. It’s the same as this perfectly valid type alias:
type Optional[T] = T | None
We definitely don’t want to extend the tag syntax to all one argument type aliases.
On that spirit we may broaden the application of tags to future special forms that aren’t necessarily 1-argument but do apply to names, by documenting that the declared type becomes the first argument when such a special form takes multiple arguments, so we can rewrite:
Following that logic (although I somewhat agree) would leave other special forms in a well, special place. Most of them just return a typing._GenericAlias after checking the arguments given. Would they be simple enough to rewrite as TypeAliases?
On a different note, would user-defined Protocols and Generics be able to be used behind the @ operator? Sometimes they can specify metadata / additional information for some annotation, but they could be ‘trash’ too. (Perhaps implementing some @typing.Tag would work to indicate which objects can be used for tags (maybe directly on methods like __call__ or __(class_)getitem__ to indicate to users and checkers that e.g. Doc should be called))
I don’t understand what you mean here. I don’t see how you could rewrite, e.g. Required as a type alias?
No. Tags should be completely separated from types. Somewhere there should be a base class Tag that indicates that a class can be used like this.
For called vs non-caller tags: my current idea is that only instances of a Tag class can be used with @, not the class itself. So Required wouldn’t be a class, but Doc Aand Deprecated would. But I am not completely sold on this.
Generalizing this syntax to arbitrary tags is a good idea and really helpful as these kinds of annotations get used more. But I think that special casing the currently available annotations like Readonly is a mistake. Making a more general parent class that differentiates tags that won’t be wrapped in an Annotated is better but imo still weird. The outer structure of what int @something results in shouldn’t depend on something.
In addition to being implemented first, the reason why Readonly[int] is spelled like that is because Readonly is primarily meant to be interpreted by type checkers, so it makes sense to write it where you’d write type information. On the other hand, Annotated[int, uint32] is primarily meant to be interpreted by runtime mechanisms, so the metadata goes into a special non-type place. I think that if we want to keep this distinction, the new syntax should reflect it by having different syntax for both kinds of metadata.
But I can also see how for most users, this distinction doesn’t really matter. Ultimately, you’re just putting tags on a name and some part of your toolchain finds some meaning in them. We’re also already blurring this line by explicitly desinging type annotations to be interpretable at runtime and letting linters and similar tools interpret Annotated metadata if they want to.
But in that case, why even bother with having different behaviours for int @something? We could just say that it always wraps tags in Annotated and that Annotated[int, Readonly] is equivalent to Readonly[int]. Of course, having two possible representations would increase the complexity needed to parse annotations at runtime, but we already have many similar situations (e.g. int | None vs Optional[int]) and utility functions in the typing module that normalize them.
+ has another benefit here in that it could work even in cases without a type hint, (which is one of the current detractions of both Annotated, and the proposed use of @) and allow use with only annotated data that isn’t type system data without needing to do typing.Any + ... thanks to unary + (which could be implementable with __pos__ on metadata objects intended to exist in annotations)
the downside here would be that this can be viewed as unintuitive or an operator abuse when considering it in the context of the current set-theoretic model for typing (why isn’t this set addition?)
I think it’s a fine option, but it does come with a “people need to learn this” that differs from what someone might intuit for addition here, where @ likely does not (nobody is likely to assume an attempt at matrix multiplication, even though @ as a binary operator is for that currently)
Yes, perhaps having calling syntax differ from generic syntax is something we’ll need to do. Perhaps Required, Doc, (user defined objects??), … can return some _RequiredAlias instance. If the instance inherits from Generic (or uses the new syntax), the tag should be used with <tag>[...] syntax, otherwise it should be called.
Generally, although this post seems to drift onto some other syntax, I think the original idea should be kept. The exact imlematio details, how users can define their own tag objects, and similar, should be discussed later on.
Unless i’m missing something, I think it will be very unfortunate if individual classes need to implement something in order to support this. The goal is specifically for inclusion into Annotated.
Insofar as certain special forms are relevant to the discussion, I would think they’re a typechecker special case one way (int @Required → Required[int]) or another (int @Required → Annotated[int, Required]). But at the end of the day, typecheckers will need to be aware of the specific translation semantics of what the syntax means. So it’s not like a normal class could do anything other than what the default type.__rmatmul__ method would do without differing in real-world behavior from what the typecheckers think is going on.
So if we’re going with this decorator-like syntax, we should avoid confusion with actual decorators. For example:
@final # function decorator
@Final # type tag
@deprecated("msg") # function decorator
@Deprecated["msg"] # type tag
One way to fix this is to make these aliases of each other (or otherwise interchangeable), and have them support both decorating functions and tagging types.
I want to avoid the typing confusion we had with list and List meaning different things.
I think a tag (and anything else used for annotations), should be spelled uppercase (Abc, unless they are builtins), whilst decorators (mostly implemented as functions (although they can be types)) should be written as abc.
We could however try to make names of runtime decorators differ from names of tags.