Your worker uses from __future__ import annotations and reads annotations at startup on Python 3.14 — should that import stay?
answer
- Two ways to avoid definition-time evaluation
- One erases the expression, one postpones it
- What does a runtime consumer get back?
- A string carries no enclosing scope
- Removal is bundled with the 3.14 version floor
basics
~20 sDrop it once your minimum version is 3.14. The future import stringifies every annotation permanently, so runtime consumers must re-resolve text; 3.14's deferred evaluation gives the same freedom from definition-time errors while handing back real objects.
solid answer
~50 s`from __future__ import annotations` implements PEP 563: every annotation is stored as a plain string, forever. It solved definition-time `NameError` and cut import cost, but it forced every runtime consumer to resolve text back into objects, and a string carries no scope — an annotation naming a local of an enclosing function simply cannot be resolved later. PEP 649 in Python 3.14 solves the same two problems by deferring evaluation instead of erasing it: the expressions live in a lazily-called `__annotate__` function, so definition is still cheap, forward references still work unquoted, and what you read back are real objects. The future import still functions on 3.14 and nothing forces you off it, but it is superseded. The one hard constraint is your version floor: remove it only when you no longer support 3.13, because there the unquoted forward references it was hiding become real errors.
code
python · 14 linesfrom __future__ import annotations
import typing
def outer():
class Local: ...
def inner(v: Local) -> None: ...
return inner
g = outer()
print(g.__annotations__) # {'v': 'Local', 'return': 'None'}
try:
typing.get_type_hints(g)
except NameError as exc:
print("unresolvable:", exc)go deeper
Know that from future import annotations turns annotations into strings, and that Python 3.14 achieves the same forward-reference freedom without doing so. You are unlikely to be asked to plan the migration.
Explain the concrete difference in what annotations holds under each scheme, and why string annotations force every runtime consumer to resolve text back into objects.
An interviewer expects operational judgement: tie removal to the supported-version floor, name what breaks if you go early, and refuse to leave a broad except around a startup pass that reads annotations.
Own the sequencing across a fleet and a set of published libraries — when the floor moves, whether libraries must span both schemes, and which layer is permitted to force annotation evaluation at startup at all.
### The two mechanisms, stated plainly **PEP 563**, opt-in since Python 3.7 via `from __future__ import annotations`, makes the compiler store every annotation as its source text. `__annotations__` becomes a dict of strings. Nothing is ever evaluated unless someone explicitly evaluates it. **PEP 649** (amended by PEP 749) is the Python 3.14 default. The compiler stores the annotation *expressions* in a lazily-called `__annotate__` function. Nothing is evaluated at definition time either — but when it finally is, you get objects. Both remove definition-time cost and both make unquoted forward references work. They differ entirely in what a runtime consumer receives. ### The worker in the question Picture an image-thumbnail worker whose startup pass walks its handler classes, reads their annotations, and builds a coercion table from them. Under the future import those annotations are strings. So the pass has to resolve each string against some namespace, and resolution can fail — a name behind a type-checking-only import, a class from a module that has not finished importing. If that pass is wrapped in a broad `except Exception: pass`, and someone at some point wrote one, the failure is swallowed: the worker starts, the coercion table is silently half-empty, and the first malformed job payload flows straight through unvalidated. The bug is invisible because the only evidence was an exception nobody kept. The cold-start story pulls the same way. A 45-second cold start makes people reach for anything that shortens import, and the future import was one of those things — it stopped annotations being evaluated at import. On 3.14 you get that saving natively: annotating a function attaches a small code object and evaluates nothing. The import-time argument for PEP 563 is simply gone. ### Why real objects beat strings A string annotation is a name with its context amputated. Resolving `"Local"` later means guessing a namespace, and there are cases where no namespace works — an annotation that refers to a class defined inside an enclosing function is unresolvable from a string, because that scope is not reachable from module globals. The 3.14 annotate function is a closure over the scope where the annotation was written, so the same case resolves without special handling. Strings also push errors somewhere with no useful traceback: you find out that a name is bad inside your resolver, not at a frame that says which annotation it was. ### The fix for the worker Three changes, in order. Delete the future import once the version floor allows. Replace the swallowing `except Exception: pass` with a narrow handler that logs the annotation and the object it belonged to. Then decide deliberately how much evaluation the startup pass wants: strict evaluation when a missing type is a real bug you want to abort on, or the tolerant `annotationlib.Format.FORWARDREF` mode when some names legitimately do not exist yet and you intend a second resolution pass after imports settle. ### The constraint that actually decides it The future import is doing real work for you on older interpreters. Delete it while still supporting 3.13 and every unquoted forward reference in the codebase becomes a definition-time `NameError` there. So the removal is bundled with the version bump, not done ahead of it. If you publish a library that must span 3.11 through 3.14, keep the import or keep the quotes, and write your introspection so it copes with both string and object annotations. ### What removal actually changes at runtime Annotations start being evaluated when read. If an annotation expression is expensive — a subscripted generic built by a helper call, say — that cost reappears at read time. It is small in practice and paid once, but it is not literally zero, and code that previously relied on annotations being inert strings (comparing them as text, storing them in a config, feeding them somewhere that expects `str`) will break loudly. That is a straightforward thing to grep for. ### What not to claim The future import has not been removed and no removal version has been announced; on 3.14 it still works exactly as it always did. "Superseded" is the accurate word: 3.14 gives you the benefits without the cost, so new code should not reach for it, and existing code should shed it when the floor moves. ### The compact answer PEP 563 traded real annotation objects for cheap definitions; PEP 649 in 3.14 gives you cheap definitions and keeps the objects. Drop the import when your minimum is 3.14, stop swallowing resolution failures, and choose an evaluation format explicitly rather than inheriting whatever the future import forced on you.
- What breaks first if you delete the future import while still supporting Python 3.13?Every unquoted forward reference the import was covering for. On 3.13 annotations are evaluated during the `def` or `class` statement, so a name defined later in the file raises `NameError` at import, and the module fails to load at all. Deleting the import is therefore part of raising the supported floor to 3.14, not a change you can ship ahead of it.
- Does removing the import make imports measurably slower?No. Definition-time evaluation does not come back: 3.14 attaches a small code object per annotated object and evaluates nothing until something reads the annotations. What returns is the evaluation cost at read time, paid once, only by code that actually introspects. For a service that never reads its own annotations the difference is noise.
- What code is most likely to break when annotations stop being strings?Anything treating an annotation as text: comparing it to a literal, doing substring checks on it, serialising it into config or a schema, or passing it somewhere that expects `str`. Also any resolver that assumed it must evaluate strings and now receives objects. Both classes are easy to find by grepping for reads of `__annotations__` and for calls into your own hint-resolution helpers.
The future import is photographing every document and shredding the originals; deferred evaluation just leaves them in a drawer you open when you need them.
saying these in an interview costs you the question
- Says the future import is required for forward references on 3.14
- Claims the future import was removed in 3.14
- Thinks PEP 563 and PEP 649 store the same thing
- Deletes the import while still supporting 3.13
- Believes string annotations can always be resolved later
- Keeps a broad except around annotation resolution