Representative interview topic

Coding interview: How would you introspect Python 3.14 annotations without accidental evaluation?

CodingHard
Offer.cc Editorial TeamPublished Updated

Question

A plugin loader inspects function annotations to build a schema. After upgrading to Python 3.14, some annotations import optional modules or fail on unresolved names. How would you redesign introspection so discovery is predictable and safe?

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.

Public sources

Related questions

Related interview tool

Use Screenshot for a coding prompt

Capture the problem, then work through the constraints, solution, code, edge cases, and complexity in order.

View the tool