Thanks for providing some context. My read of the previous discussion is that @ was a favored candidate. People liked the int @ Range(...) syntax, but consensus stalled over the ambiguity of chaining (e.g., int @ ann1 @ ann2).
A mandatory list literal (int@[ann1, ann2]) avoids those concerns.
list[int] looks like subscripting. Are there any cases where this would be really ambiguous? My initial instinct is that you wouldn’t use matrix multiplication where type annotations are valid or vice versa.
In this case both operands are types. In case of Annotated, the RHS can be anything, including integers.
Plus, unlike other type operators, using @ for Annotated would be unique to Python. The Python syntax for subscripting (and union’ing) generic types is very common in other languages, and is even used in in type theory (e.g. in the “F” type system (wiki)).
I really like the bracket-free syntax. The common case will likely be a single annotation (e.g., int @ Range(...)), and removing the brackets significantly reduces visual clutter.
It maintains a clear ‘inline decorator’ mental model and avoids the need to dive into complex type theory. This is a crucial distinction: Annotated was designed as a container for metadata (like Doc) that is often strictly outside the type system.
Technically:int @ Doc(...) @ Range(...) would flatten to Annotated[int, Doc(...), Range(...)]. This is consistent with current typing behavior and ensures we aren’t creating deep nested objects at runtime.
Joren: How do you feel about the bracket-free version? To me, it actually looks less like matrix multiplication than the list-based version did.
I’ll have a CPython prototype ready shortly, followed by a Mypy patch. I think seeing the low implementation complexity will help us move toward a formal PEP.
Seems less confusing, but it still looks a bit awkward to me.
That said, I’m probably not the right person to ask, since I almost never use Annotated anyway (I spend most of my time in stubs).
@Jelle , now that I have a working CPython prototype and a Mypy patch, I’d like to move forward with a PEP. The implementation handles all the edge cases I could identify; I believe it’s ready for a formal review (EDIT: It’s not quite there, I’d like to replace typing.Annotated with the new native c class instead of having both classes live side by side).
If you’re willing to sponsor, I can get a PEP number assigned and will have an .rst draft ready to share here ASAP.
I had this comment about a runtime issue in the previous thread that I guess I’d need to re-raise:
In short, presuming @ for Annotated works the same way as | for Union, if the type is a forward reference the entire annotation becomes an opaque forward reference under Format.FORWARDREF. This would potentially lose all of the metadata until the forward reference could be resolved (see the first example from that link).
Personally I prefer the explicit Annotated (both due to the runtime issues and for clarity), I would like it more if typing was a lighter import but I’d rather that be a driver for improving the import time of typing over adding more new syntax to avoid the import.
@DavidCEllis Interesting corner case. This is happening because the Stringifier in annotationlib doesn’t handle __or__ specially.
I’ve added handling for @ and this test case:
It handles or in the way it does because doing otherwise requires making the additional assumption that the unknown object is a type. It wouldn’t be an unreasonable assumption, but it’s not required that the objects in annotations are types.
That wasn’t my implication, it’s that without making the additonal assumption that undefined here is a type and isn’t a numpy array itself.
undefined @ np.array([[2]]) @ np.array([[3]])
would resolve to ForwardRef('undefined @ __annotationlib_name_1__ @ __annotationlib_name_2__', ...)
while Annotated[undefined, np.array([[2]]), np.array([[3]])]
is unambiguous and can resolve to typing.Annotated[ForwardRef('undefined', ...), array([[2]]), array([[3]])]
Currently annotationlib doesn’t make this assumption for unions so doing so for Annotated is a change.
class WeirdMeta(type):
def __matmul__(cls, other):
return type(other)
class undefined(metaclass=WeirdMeta):
pass
where the “correct” resolution is actually np.Array[1]
I would argue that that is actually an incorrect way to resolve that type hint. Because if type checkers and run-time type users need to deal with such behaviour that’s a great cost, and it’s not going to gain us anything beyond the joy of the forbidden. But I know I’m going to lose that argument ↩︎
Sure, if you’re requiring that undefined is a type. Otherwise a simpler solution would be that undefined is np.array([[1]]).
Type checkers would require the argument to be a type but the runtime doesn’t and as mentioned before, annotationlib doesn’t make that assumption for unions.
Exactly and I could see an argument against assuming annotations are types in annotationlib. IMHO this is a reasonable constraint for FORWARDREF, we should go ahead with @ and update the documentation accordingly.