TypeForm: Spelling for a type annotation object at runtime

Regarding recognizing TypeForm values, @erictraut did you see the outline of a solution I recorded for you in this video explanation?

To summarize:

  • Several literal forms could be immediately recognized as literal TypeForm values
    • all of the following literal forms → TypeForm → object
      • Any, Self, LiteralString, NoReturn, Never, Literal[…], Optional[…], Union[…], Callable[…], Tuple[…], Annotated[…], TypeGuard[…], TypeIs[…], TypeForm[…]
  • Name references can presumably be looked up in some kind of symbol table which already knows which names correspond to class objects, type aliases, and type vars. All such names spell TypeForm values:
    • name (class) → type[] → TypeForm → object
    • name (type alias) → TypeForm → object
    • name (type var) → TypeForm → object
  • None is an ambiguous case (†) because it has different interpretations as a value expression vs. a type expression:
    • None † → None (value expression)
    • None † → NoneType → TypeForm → object (type expression)
    • :speech_balloon: Perhaps a literal None could be recognized as a special constant which would be usable in both a value and a type context?
  • String literals are also an ambiguous case (†) and are truly difficult to find a solution for:
    • string literal † → str, Literal[…] (value expression)
    • string literal † → TypeForm → object (type expression)
    • string literal † → annotation → object (annotation expression)
    • :speech_balloon: One possible solution is to ban fully stringified types (which appear as string literals) from being recognized as a TypeForm value. Not ideal because that would cause the definition of TypeForm to deviate from a “type expression” slightly, but I’m open to such a deviation if it unblocks the rest of the proposal.
  • expr | expr is the final ambiguous case (†):
    • expr1 | expr2 † → object (value expression) = binary-OR
    • expr1 | expr2 † → TypeForm → object (type expression) = union type
    • :speech_balloon: Assuming a type checker already has logic to infer the types of the expr1 and expr2 arguments, if both sides are inferred to be of TypeForm type then the entire |-expression could be inferred as TypeForm as well.
  • (No other kinds of type expressions exist.)

I think you’re looking at draft 1. Draft 2 says this:

The runtime representations of type-forms are not currently defined by an API considered implementation details that may change over time and therefore static type checkers are not required to recognize them:

IntTreeRef: TypeForm = ForwardRef('IntTree')  # ERROR: Runtime-only form
ListOfInt: TypeForm = types.GenericAlias(list, int)  # ERROR: Runtime-only form

Edit: Updated link to point at not just draft 2 but also the explanation of how it differs from draft 1.


Agreed. Draft 2 already limits TypeForm to matching type expressions only (and not annotation expressions, which would include type qualifiers).

Draft 3 (in progress) plans to make TypeForm exactly match type expressions, and not a subset of them.


Interesting. Let me hear your response to the video explanation above first. Then I might further explore this idea of explicitly marking type-forms with a surrounding TypeForm(...).