Here’s one way this could be done. Define a method called __annotate__
and allow subclassing Annotated like,
class MyAnnotated(Annotated):
def __annotate__(self, typ: type, metadata: object, other_metadata: object):
...
annotate here would define the type arguments allowed to Annotate[…]. Only positional arguments would be allowed and existing Annotated is like signature I put where it takes at least 2 arguments, first must be type, rest no restrictions. To define custom annotated that only allows str as first type then,
class Pattern(Annotated):
def __annnotate__(self, typ: type[str], pattern: str):
...
This would then be used like,
foo: Pattern[str, r"[0-9]+"] # Type checker would just treat this as str.
The first argument of Annotated must always be annotated with type, but can be used to restrict type by having type[X]
. Other arguments can be constrained as needed. From type checker view it can always treat any Annotated subclass as only part of type for foo is first argument. annotate only adds addition type constraints for that specific subclass usage. subclassing here is mainly just to specify expected type signature of Annotated arguments.
For pattern specifically that might work in type system today using phantom types. In PEP 695 (new generic syntax),
type Pattern[T: type[str], P: str] = T
foo: Pattern[str, Literal["[0-9]+"]]
foo type simplifies to just str since type alias doesn’t use P, but at runtime the full annotation is available still. This also type checks that first argument to Pattern is str/subtype and second argument is of type str. This does really on Literal allowing str values to be used and if you wanted to allow floats as 1 argument instead wouldn’t work out. If type vars allow something similar to bound except where values were allowed instead of types (bound_value) so that you could do (mixing old type var vs new type var syntax),
T = TypeVar('T', bound=float)
f = TypeVar('f', bound_value=float)
FloatConstraint[T, f] = T
FloatConstraint[float, 5.0] # Valid
This looks somewhat like dependent types, but if you forbid f (any type variable using bound_value) from being on right hand side of type alias most of dependent type complexity (and power) goes away.
I do have internal library that’s been making heavy usage of Annotated and could make use of this. In my case I only have one common constrained Annotated that’d have pretty simple signature of second argument is always a str, third argument is optional and if present specific metadata type. It would mostly catch typos where I accidentally put an extra comma in a long string. __annotate__
way while I think it’d work feels a bit more complex then needed as generic arguments aren’t really a function signature (keyword arguments don’t exist). Other aspect is I know Annotated is weird in type system as one of the only things that allows non type arguments to be in annotations and tends to have edge cases for type checkers to deal with. So I’d guess on static typing side simplest way to support this would be desirable vs having more weird rules.