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 Annotated
instance):
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:
A 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.
An AnnotationList
class (Not a subclass of BaseAnnotation
, probably) that would be constructed by an __matmul__
/__rmatmul__
overload of BaseAnnotation
such that arbitrary annotations can be chained.
A function 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:
class S(ctypes.Structure):
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 Any
.
(Specifically the @
syntax was also half-proposed in this comment, but I came up with it independently.)