What do annotationlib.Format's VALUE, FORWARDREF and STRING each return?
answer
- Three intentions, three formats
- One evaluates, one tolerates, one never evaluates
- What replaces a name it cannot resolve?
- A placeholder you can evaluate later
- VALUE, FORWARDREF, STRING
basics
~20 sVALUE evaluates the annotation expressions for real and raises NameError on an unresolvable name. FORWARDREF evaluates what it can and substitutes an annotationlib.ForwardRef for the rest. STRING returns the annotation source text without evaluating anything.
solid answer
~40 sPython 3.14's `annotationlib` module exposes a `Format` enum that you pass to `annotationlib.get_annotations`. `Format.VALUE` is the default and matches plain `__annotations__` access: real objects, and a `NameError` if a name is not bound. `Format.FORWARDREF` is the tolerant mode — everything resolvable comes back as an object, anything that fails is wrapped in an `annotationlib.ForwardRef` you can call `.evaluate()` on later. `Format.STRING` never evaluates anything; it reconstructs the annotation source as text, which is what documentation and signature-rendering tools want. There is a fourth member, `VALUE_WITH_FAKE_GLOBALS`, which is the internal mechanism `annotationlib` uses to synthesise the other two formats from an annotate function that only implements `VALUE` — user code never passes it.
code
python · 7 linesimport annotationlib
from annotationlib import Format
def resize(img: Thumbnail, size: int) -> None: ...
print(annotationlib.get_annotations(resize, format=Format.STRING))
print(annotationlib.get_annotations(resize, format=Format.FORWARDREF))go deeper
You mainly need to know that Python 3.14 lets you ask for annotations in more than one form, and that the plain annotations read is the strict one that can raise NameError.
Be able to name the three usable formats and say precisely what each returns for a name that is not defined: an exception, a ForwardRef placeholder, or the source text.
Show the judgement: pick FORWARDREF for an import-time pass over user code that must not abort, STRING for anything that renders or logs, VALUE where a missing type is a real bug you want raised.
Own the introspection policy across your stack — which layer is allowed to force evaluation, how two-pass resolution is coordinated, and how tooling that must also run on 3.13 abstracts over the absence of these formats.
### Why more than one format exists Once annotations are evaluated lazily, a runtime consumer gets a choice the old eager model never offered: *how much evaluation do I actually want?* A validator building coercion functions wants real classes. A documentation renderer wants text and would rather not import anything. A framework registering half-defined models wants whatever resolves now and a placeholder for the rest. Python 3.14's `annotationlib.Format` enum names those three intentions, plus one internal mode. ### Format.VALUE The default. It runs the object's annotate function normally, so every annotation expression is evaluated against the scope it was written in. You get real objects — `int`, a class, a subscripted generic — and you get a `NameError` the moment any name is unbound. This is exactly what reading `__annotations__` does; `get_annotations(obj)` with no format argument is the same thing with a nicer API around it. Use it when the code is genuinely going to *use* the types and a missing one is a bug you want to hear about. ### Format.FORWARDREF The tolerant mode, and the one worth remembering. It evaluates what it can and wraps whatever fails in an `annotationlib.ForwardRef` object rather than raising: ```python import annotationlib from annotationlib import Format def resize(img: Thumbnail, size: int) -> None: ... anns = annotationlib.get_annotations(resize, format=Format.FORWARDREF) print(anns) # {'img': ForwardRef('Thumbnail', owner=<function resize ...>), # 'size': <class 'int'>, 'return': None} ``` Notice the mix: `size` and `return` resolved to real objects, only the unknown name became a placeholder. The `ForwardRef` remembers where it came from, so once the missing name is defined you can call `.evaluate()` on it and get the real object: ```python class Thumbnail: ... print(anns["img"].evaluate()) # <class '__main__.Thumbnail'> ``` This is the format for a library that inspects user classes during import, when some referenced names legitimately do not exist yet — the classic circular-model problem. It turns a hard failure into a deferred one you control. ### Format.STRING No evaluation whatsoever. Continuing the same module, you get the annotation back as source text: ```python print(annotationlib.get_annotations(resize, format=Format.STRING)) # {'img': 'Thumbnail', 'size': 'int', 'return': 'None'} ``` Two things to note. First, it works even when nothing is importable and nothing is defined, which makes it safe for documentation generators, signature printers and static tooling that must not execute user code. Second, the text is regenerated from the compiled form rather than copied byte-for-byte from the file, so whitespace and some formatting are normalised. Treat it as a faithful rendering of the expression, not as a verbatim quote of the source line. ### Format.VALUE_WITH_FAKE_GLOBALS The implementation detail. An annotate function is only obliged to implement `VALUE`; when asked for something else it may raise `NotImplementedError`. `annotationlib` then re-invokes it with a specially-prepared globals mapping whose lookups produce proxy objects, and from the result it constructs the string or forward-reference form. That is the mode `VALUE_WITH_FAKE_GLOBALS` names. You should never pass it to `get_annotations`; know it exists so you recognise it in the enum and in `NotImplementedError` handling inside hand-written annotate functions. ### Choosing between them A short decision rule: if you are going to *call* something with the type, take `VALUE` and let it fail loudly. If you are building a registry over user-defined objects during import and want to finish the pass, take `FORWARDREF` and evaluate the placeholders on a second pass. If you are rendering or logging, take `STRING` and execute nothing. ### The boundary with the older API `annotationlib.get_annotations` is about *retrieving* annotations in a chosen form. It does not walk a class's bases, apply `Optional` normalisation, or take explicit namespaces the way the older resolution helper in `typing` does. Reach for the format enum when the question is "how much do I want evaluated"; reach for the resolution helper when the question is "resolve these hints against this namespace". ### Version note The whole module is new in Python 3.14. On 3.13 and earlier there is no `annotationlib` and no format concept: annotations are either real objects or, under the old string-annotation future import, strings, with no third option.
- Is the text returned by Format.STRING identical to what is written in the source file?Not byte-for-byte. It is regenerated from the compiled annotation rather than sliced out of the file, so spacing and some formatting are normalised. It is a faithful rendering of the expression — good enough for documentation, signature display and logging — but you should not compare it against source text or rely on it for anything that needs exact provenance.
- Why would a library prefer FORWARDREF over letting VALUE raise?Because during an import pass some referenced names genuinely do not exist yet — mutually referencing classes are the standard case. FORWARDREF lets the pass complete, resolving everything it can and leaving a `ForwardRef` placeholder for the rest, which the library evaluates on a later pass once the module has finished loading. VALUE would abort the pass on the first unresolvable name.
- What is VALUE_WITH_FAKE_GLOBALS for, and should you pass it?Never pass it. It is the internal mode `annotationlib` uses when an annotate function raises `NotImplementedError` for a format it does not support: the machinery re-runs the function against a prepared globals mapping that yields proxy objects, and builds the STRING or FORWARDREF result from what comes back. It exists so hand-written annotate functions only have to implement VALUE.
Reading a recipe three ways: cook it now, cook what you have and leave sticky notes for the missing ingredients, or just photocopy the page.
saying these in an interview costs you the question
- Thinks FORWARDREF returns strings like STRING does
- Says STRING evaluates the expression then calls repr
- Believes VALUE silently skips names it cannot resolve
- Passes VALUE_WITH_FAKE_GLOBALS as a normal option
- Assumes a ForwardRef can never be resolved later