[python 3.14] Metaclasses: interact with annotations from `namespace` dict

Hi everyone,

After having read the python3.14 “What’s new” about annotations and both related PEPs 649 and 749, I have a question about how to interact with annotations from a metaclass’ __prepare__ or __new__.

The simplest first: How to retrieve annotations from a namespace dict? Is the following ok?

from annotationlib import Format

def get_namespace_annotations(namespace: dict[str, object]) -> dict[str, object]:
    """Get annotations from the namespace dict."""
    if (ann := namespace.get("__annotate__", None)) is not None:
        return ann(Format.VALUE)  # Change to FORWARDREF when implemented (?)

    return  cast("dict[str, object]", namespace.get("__annotations__", {}))

Now the second question would be: How to manipulate its annotations (i.e. add/remove/modify)?

I guess the “legacy” way would be to just use get_namespace_annotations, then set __annotate__ to None, before using the obtained dict as in legacy. Is that even ok for a start?

And of course, is there a “cleaner” way, interacting directly with __annotate__?

I worry mainly because of this:

The behavior of accessing the __annotations__ and __annotate__ attributes on classes with a metaclass other than builtins.type is unspecified. The documentation should warn against direct use of these attributes and recommend using the annotationlib module instead.

Similarly, the presence of __annotations__ and __annotate__ keys in the class dictionary is an implementation detail and should not be relied upon.

Thanks!

I’m not sure about getting the __annotate__ function, I’m currently doing roughly the same as you in order to get the function itself in my own metaclass. If it stops being available in the namespace then I’m not sure how you’re intended to obtain them in __new__.

I’d note that you shouldn’t call it directly with Format.FORWARDREF in most cases, you’ll want to use annotationlib.call_annotate_function(ann, Format.FORWARDREF).

Oh that’s a very good point thanks! I missed that despite looking at the source lol. Without any owner I guess since it does not exist yet inside __new__?

I guess a possibility is to redefine __annotate__:

def new_annotate(added_annotations: dict[str, object]) -> Callable[[int], dict[str, object]]:
    """Adds annotations manually."""
    def inner(mode: int) -> dict[str, object]:
        return __annotate__(mode) | added_annotations
    return inner

But again, I’m a bit lost on what the recommended practice is…

If this ever helps someone, for now I use the following

from annotationlib import Format, call_annotate_function  # type: ignore[import-not-found]
from typing import cast

def get_namespace_annotations(namespace: dict[str, object]) -> dict[str, object]:
    """Get annotations from a namespace dict in python3.14."""
    __annotations__ = cast("dict[str, object] | None", namespace.get("__annotations__"))
    __annotate__ = namespace.get("__annotate__")

    if __annotations__ is None:
        if __annotate__ is None:
            return {}
        return call_annotate_function(__annotate__, Format.FORWARDREF)

    # Avoid bad synchronization, suppress `__annotate__`
    if __annotate__ is not None:
        namespace["__annotate__"] = None

    return __annotations__

which seems to work but does not sanitize __annotate__ and __annotations__.

This works with the current alpha, but in general I don’t want people to peek at the internal implementation details regarding where things are stored. I just put up a PR (gh-132261: Store annotations at hidden internal keys in the class dict by JelleZijlstra · Pull Request #132345 · python/cpython · GitHub) that will make it so you can call annotationlib.get_annotate_function(ns) to get the annotate function out of the namespace.

Your function also immediately calls the annotate function. This has the downside that if the class contains annotations that cannot resolve immediately, this will fail. You can use annotationlib.call_annotate_function(ann, Format.FORWARDREF) to address this. This is what I’d use if I need to access the annotations at class creation time. For example, this is what the implementation of typing.TypedDict does on current main.

I’d generally recommend to write a wrapper that evaluates the annotations and makes the changes you want. Example:

import annotationlib

class Meta(type):
    def __new__(mcls, name, bases, ns):
        annotate = annotationlib.get_annotate_function(ns)
        typ = super().__new__(mcls, name, bases, ns)

        def wrapped_annotate(format):
            annos = annotationlib.call_annotate_function(annotate, format, owner=typ)
            annos["Meta"] = "example"
            return annos
        
        typ.__annotate__ = wrapped_annotate
        return typ

I will probably add some recipes to the annotationlib docs with examples like this.

General caution: We’re still in the alpha phase, and anything may still change.

Thanks for your message. Did you take into account my last message just before yours? I think it essentially implements it the way you meant it?

Also, I just looked up your PR, it seems to check whether ns is a dict, shouldn’t it check for Mapping only since __prepare__ could choose any type of mapping?

Regarding modification I agree that it is a possible way, but it seems a bit wonky. I know this is in alpha phase but I’m honestly kind of surprised the PEPs were accepted in their current state, when they leave so much room for debate while breaking a lot of __annottions__ use.