This is a half baked idea I got from reading the recent discussions about PEP-727. One of the big concerns raised in there is that the usage of
Annotated means that any usage of
doc requires a type hint, even if that isn’t wanted otherwise, and more generally, that annotations are supposed to be type information and documentation isn’t really that.
My suggestion would be to use an AFAIK currently unused syntax in annotations for non-typing purposes, specifically function calls:
def create_user(name: Doc("The User's name")): ...
If you want both type hints and non-typing annotations, the
@ operator could be overloaded to create an
AnnotationList class (or an
def create_user(name: str @ Doc("The User's name")): ...
The stdlib would gain a new module, for example named
annotations with a few tools:
BaseAnnotation class that would act similar to the
BaseException: It would store passed args and kwargs and have a repr that looks similar to a dataclass repr, i.e. such that it can be reconstructed.
Doc would then be a subclass (maybe also in the same module) that would verify that only one argument was passed.
AnnotationList class (Not a subclass of
BaseAnnotation, probably) that would be constructed by an
__rmatmul__ overload of
BaseAnnotation such that arbitrary annotations can be chained.
get_annotation or similar, that takes a (tuple of) subclass(es) of
BaseAnnotation and an arbitrary entry of an
__annotations__ dict (or an entire dict/object with annotations?) and extracts the corresponding annotation instance(s) or returns
None/raises an exception. This would deal with the different options of the annotation being passed in being a string, a type hint with no annotations, a direct
BaseAnnotation subclasses instance or an
AnnotationList instance. (I didn’t think about how this would play with delayed annotation evaluation, this might need to be changed)
Either here or in
typing there would be a magic
AnnotationAlias type marker like the
TypeAlias marker, which basically the same semantics. However,
TypeAlias would not carry over the new annotations, but only the actual type (for static type checkers at least), so that an
@ Doc() on a
TypeAlias would document the alias and not the parameters where the new name is used.
Static Type checkers/linters should assume that any call syntax in an annotation is not a type hint and ignore it. AFAIK that wouldn’t cause any conflicts. The type checkers can also check that the calls are correct
Annotation subclasses with correct parameters.
This proposal would in the long term mean that
Annotated could be deprecated since it’s purpose would be fully contained by this syntax.
For example, ctypes could use the syntax like this:
a : int @ CType(ctypes.c_int)
b : CType(ctypes.c_char_p)
This would still mean that non-typing users of annotations are second class, but it would allow usage of annotations without any type hints without conflicting with type checkers. If no type is specified in an annotation, type checkers should assume
@ syntax was also half-proposed in this comment, but I came up with it independently.)