PEP 0593 added the ability to add arbitrary user-defined metadata to type annotations in Python.
At type-check time, such annotations are… inert. They don’t do anything.
Annotated[int,
X]
just
means int
to the type-checker, regardless of the value of X
. So the entire
purpose of Annotated
is to provide a run-time API to consume metadata, which
integrates with the type checker syntactically, but does not otherwise disturb
it.
Yet, the documentation for this central purpose seems, while not exactly absent, oddly incomplete.
The PEP itself simply says:
A tool or library encountering an Annotated type can scan through the annotations to determine if they are of interest (e.g., using
isinstance()
).
But it’s not clear where “the annotations” are, given that the PEP’s entire
“consuming
annotations” section
does not even mention the __metadata__
attribute where the annotation’s
arguments go, which was only even added to CPython’s
documentation. Its list of
examples just show the repr()
of the relevant type.
There’s also a bit of an open question of what, exactly, we are supposed to
isinstance()
-ing here. If we want to find arguments to Annotated
,
presumably we need to be able to detect if an annotation is an Annotated
.
But isinstance(Annotated[int, "hello"], Annotated)
is both False
at
runtime, and also a type-checking error, that looks like this:
1 |
|
The actual type of these objects, typing._AnnotatedAlias
, does not seem to
have a publicly available or documented alias, so that seems like the wrong
route too.
Now, it certainly works to escape-hatch your way out of all of this with an
Any
, build some version-specific special-case
hacks
to dig around in the relevant namespaces, access __metadata__
and call it a
day. But this solution is … unsatisfying.
What are you looking for?
Upon encountering these quirks, it is understandable to want to simply ask the
question “is this annotation that I’m looking at an Annotated
?” and to be
frustrated that it seems so obscure to straightforwardly get an answer to that
question without disabling all type-checking in your meta-programming code.
However, I think that this is a slight misframing of the problem. Code that
is inspecting parameters for an annotation is going to do something with that
annotation, which means that it must necessarily be looking for a specific
set of annotations. Therefore the thing we want to pass to isinstance
is not
some obscure part of the annotations’ internals, but the actual interesting
annotation type from your framework or application.
When consuming an Annotated
parameter, there are 3 things you probably want to know:
- What was the parameter itself? (type: The type you passed in.)
- What was the name of the annotated object (i.e.: the parameter name, the
attribute name) being passed the parameter? (type:
str
) - What was the actual type being annotated? (type:
type
)
And the things that we have are the type of the Annotated
we’re querying for,
and the object with annotations we are interrogating. So that gives us this
function signature:
1 2 3 4 5 |
|
To extract this information, all we need are
get_args
and
get_type_hints
;
no need for
__metadata__
or
get_origin
or any other metaprogramming. Here’s a recipe:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
It might seem a little odd to be blindly assuming that get_args(...)[0]
will
always be the relevant type, when that is not true of unions or generics.
Note, however, that we are only yielding results when we have found the
instance type in the argument list; our arbitrary user-defined instance isn’t
valid as a type annotation argument in any other context. It can’t be part of
a Union
or a Generic
, so we can rely on it to be an Annotated
argument,
and from there, we can make that assumption about the format of get_args(...)
.
This can give us back the annotations that we’re looking for in a handy format that’s easy to consume. Here’s a quick example of how you might use it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Acknowledgments
Thank you to my patrons who are supporting my writing on this blog. If you like what you’ve read here and you’d like to read more of it, or you’d like to support my various open-source endeavors, you can support my work as a sponsor! I am also available for consulting work if you think your organization could benefit from expertise on topics like “how do I do Python metaprogramming, but, like, not super janky”.