Prompt and scope
Python 3.14 adds annotationlib for retrieving annotations when evaluation is deferred. The loader must support forward references, avoid executing arbitrary annotation expressions during discovery, and provide a compatibility path for older Python versions. The core skill is runtime introspection and side-effect control, so this is a coding question.
What interviewers assess
Strong answers distinguish retrieving annotations from evaluating them. They choose VALUE, FORWARDREF, or STRING based on the consumer, isolate failures, and define when trusted code may be evaluated. They also address cache invalidation, module reloads, nested type aliases, and a versioned fallback rather than relying on private interpreter attributes.
Questions to clarify first
- Is the loader inspecting trusted application code or third-party plugins?
- Does the schema generator need runtime objects, strings, or unresolved references?
- Which Python versions must remain supported?
- Are annotation expressions allowed to import modules or call functions?
- How are failures reported, cached, and retried after a plugin is reloaded?
- Is annotation data part of a public compatibility contract?
30-second answer framework
“I would stop calling a helper that always evaluates annotations. On Python 3.14 I would use annotationlib.get_annotations() with Format.FORWARDREF or Format.STRING for untrusted discovery, and reserve Format.VALUE for an explicit trusted phase. I would normalize results into an internal schema, isolate per-object failures, and keep a version-gated fallback for older Python. Tests would cover forward references, optional imports, nested aliases, reloads, and deterministic error reporting.”
Step-by-step answer
Step 1: Separate discovery from evaluation
Define two phases: discovery records names or ForwardRef objects without resolving them; resolution runs only for approved modules and symbols. This prevents schema discovery from importing optional dependencies or executing an expression merely because it was annotated.
Step 2: Select the annotation format
Use Format.STRING when the schema needs source-like text, Format.FORWARDREF when unresolved names must remain structured, and Format.VALUE only when runtime objects are required and evaluation is trusted. Treat a format choice as part of the loader’s API contract.
Step 3: Contain failures and side effects
Catch resolution errors per callable or class, attach the object path and format to diagnostics, and continue discovering independent plugins. Do not silently convert an unresolved reference into Any; that hides compatibility defects. Apply an allowlist before evaluating imports or symbols.
Step 4: Add versioned compatibility
On Python 3.14+, use the documented annotationlib API. On older versions, use the project’s existing compatibility layer and document its weaker guarantees. Avoid private annotate or interpreter internals as a long-term interface.
Step 5: Test determinism and lifecycle
Test annotations that reference later definitions, missing modules, nested generic aliases, strings, and module reloads. Assert that discovery does not import blocked modules, that repeated reads produce the same normalized schema, and that caches are invalidated when the plugin identity or source changes.
Model answer
“The bug is a phase violation: discovery is evaluating data that should only describe a schema. I would use Python 3.14’s documented annotationlib API, selecting STRING or FORWARDREF for untrusted inspection and VALUE only in an explicit trusted resolver. Each object gets isolated diagnostics, unresolved references stay visible, and an allowlist gates evaluation. A version adapter preserves older-Python behavior. Tests cover forward references, missing optional modules, aliases, reloads, side-effect imports, and stable normalized output.”
Common mistakes
- Always evaluating annotations → optional imports and expressions run during discovery → separate formats and phases.
- Converting failures to
Any→ schema drift becomes silent → retain unresolved references and diagnostics. - Using private interpreter fields → upgrades break the loader → depend on documented
annotationlib. - Sharing one global cache → reloads return stale schemas → key and invalidate by plugin identity and source.
- Resolving every plugin with full privileges → untrusted code gains side effects → use an allowlist and trusted phase.
- Testing only simple built-ins → forward references fail in production → include missing names and nested aliases.
Follow-up questions
Follow-up 1: When is VALUE appropriate?
When the consumer needs runtime type objects and the module and annotation expressions are trusted. It should be an explicit resolution phase, not the default discovery behavior.
Follow-up 2: Why keep FORWARDREF instead of strings?
It preserves unresolved references as structured values, allowing a later resolver to distinguish a missing name from an ordinary string annotation.
Follow-up 3: What does Python 3.14 change?
Annotations are deferred by default, and annotationlib provides documented formats for retrieving them. Code that assumed eager evaluation must define its desired format.
Follow-up 4: How do you support older versions?
Use a small version-gated adapter over the project’s existing fallback, document differences, and run the same contract tests on every supported interpreter.