TypeForm: Spelling for a type annotation object at runtime

Okay, sorry I didn’t have all the time to get into all of what I saw as issues with this in-depth earlier…

  1. By having Annotated implicitly strip annotations immediately and be special cased rather than having a way to handle Annotated, you force runtime libraries to handle annotated immediately and not have any helper functions it might get passed to.

  2. By having Annotated have this behavior, it’s not possible to type typing.get_args in a way that allows runtime libraries to work with annotated without a type ignore.

These two can be demonstrated pretty readily with (The alternative parameterization of TypeForm I mentioned)

def handle_user_input(
    typ: type[T] | TypeForm[Annotated, T, *Anns], user_input: str
) -> T:
    if typing.get_origin(typ) is Annotated:
        # if the type system implicitly converts to T instead, 
        # this is considered unreachable code instead
        # as Annotated isn't compatible with type
        # and this also isn't a valid assignment to typ
        typ, *annotations = typing.get_args(typ)  
    else:
        annotations = ()

    # call some helper function
    value = convert(user_input, typ)  # raises if can't convert to this type

    for annotation in annotations:
        if isinstance(annotation, Validator):
            annotation.validate(value)  # raises conditionally

    return value

This is a pretty clear case of handling a specific type form, and it’s something that was quite recently something runtime validation libraries were told they needed to wait for type form for. I don’t think people envisioned a situation where type form wouldn’t allow expressing what those libraries were doing within the type system.

If this pep does things like an implicit conversion from Annotated to the inner type rather than provide the tools to get at the inner type in a type safe manner, or the inability to type which type forms they can handle, those libraries will have lost cababilties of what they can type between that change and this pep.

The concern about type variable bounds doesn’t even need to come into play for there to be an issue with the proposed version and not allowing constraining to a specific type form.

I believe there are two main open questions on this PEP left:

  1. @erictraut is concerned about Implementability in pyright RE how to recognize a type expression reliably given that pyright needs to know in advance whether a particular expression it is looking at is a “value expression” or a “type expression”.
  2. @mikeshardmind is interested in a syntax (TypeForm[OriginType, *ArgTypes]) which would allow matching subsets of a type expression.

@erictraut please see this video explanation RE an outline of how an expression could be identified as a “type expression” vs. a “value expression” in advance.

Let me know what you think – in this discuss.python.org thread please, rather than in the older Google Doc thread.


@mikeshardmind can you point me to specific functions in existing PyPI packages which would benefit from the TypeForm[OriginType, *ArgTypes] syntax you propose, or would otherwise benefit from the ability to pattern-match on a type expression?


In the meantime I’ll start on draft 3 of this PEP, on peps.python.org.

If the above explanation and example isn’t compelling, you’re not going to find the examples I could link to compelling, and I’m largely beyond caring about arguing the point any further, If I end up having to use Any here for my own use due to this being insufficient, I will.

I commented on the gdoc w.r.t. Annotated metadata handling but I am copying my response here for visibility and posteriority.

Does discarding the metadata make it easier for type checkers to implement this? I do think there are use cases for preserving the metadata at type check time. Can we flip this and say that the metadata is preserved?

My understanding is that currently it would make no difference since type checkers ignore the metadata (i.e. X = Annotated[int, Gt(0)];assert_type(1, X) is valid and it would continue to be if metadata is not discarded, but stating now that the metadata should not be discarded would leave open the possibility that type checkers did understand metadata in the future (which PEP 593 does allow). So if it doesn’t make it much harder for type checkers and we just want to specify one behavior or the other my vote would be for preserving the metadata to avoid closing doors unnecessarily.

As we’ve stated before the use case Pydantic has for TypeForm is TypeAdapter:

def validate(tp: TypeForm[T], v: object) -> T: ...

Currently as per this proposal:

x = validate(Annotated[int, Gt(0)], 123)
reveal_type(x)  # int
assert x > 0  # type checkers do nothing

But if someone decided to support Gt(0) as recognized metadata in a type checker (which is allowed by PEP 593) then that type checker could do something like:

x = validate(Annotated[int, Gt(0), "foo"], 123)
reveal_type(x)  # Annotated[int, Gt(0)] ??
assert x > 0  # unnecessary assertion, I already know x > 0!

I recognize this is a big if, but I felt it’s worth pointing out and not shutting down now if there isn’t a strong reason to do so.

2 Likes

The current status quo is that static type checkers generally strip out the metadata part of an Annotated[T, metadata] type, such as in the code below:

x: Annotated[int, '> 0']
reveal_type(x)  # mypy: Revealed type is "builtins.int"
assert_type(x, int)

(Eric explains why in the comment starting with “No, metadata cannot be preserved.” in this thread.)

The current TypeForm proposal attempts to align with this status quo rather than change it, because there is limited benefit to changing the status quo for the applications that TypeForm is intended for in its Motivation.

Future proposals could very well alter the handling of Annotated[T, metadata] to preserve metadata. Or even introduce something entirely separate altogether, such as some kind of Refinement[...] syntax. But that’s out of scope for TypeForm.

@davidfstr, thanks for continuing to push this idea forward.

When the idea for TypeForm was initially suggested, my intuition was that it would be a pretty straightforward feature to spec and implement, but it has proven to be much more problematic than I anticipated. This is my attempt to identify and explain the issues that I see with the current formulation.


Let me start by reviewing some relevant static typing concepts.

Most programming languages that are statically or gradually typed make a clear distinction between “type expressions” and “value expressions”. Type expressions can appear only in certain locations within the grammar, and they serve a distinctly different purpose from value (runtime) expressions. In Python, these concepts were not clearly delineated in PEP 484. This led to ambiguities, confusion, and inconsistencies between type checkers. We’ve recently made significant progress (thanks in large part to @Jelle’s efforts) to define “type expression”, specify where type expressions can be used, what syntactical and semantic limitations are imposed on them, and how they should be interpreted by a static type checker. For details, refer to this section of the typing spec.

Type expressions and value expressions are treated very differently by static type checkers. They serve two very different purposes. Type expressions provide a way to “spell” a type in the type system. When a static type checker evaluates a type expression, its goal is to determine “what type is this spelling”? By contrast, when a static type checker evaluates a value expression, its goal is to determine “what is the type of this expression?”, or put another way, “what type describes the set of values that this expression can potentially produce at runtime?”. In the context of a type expression, it doesn’t really make sense to ask “what is the type of this expression?”.

Distinctly different rules are used for evaluating the two categories of expressions. Consider the following expressions and how differently they are treated by a type checker when evaluated as a type expression and a value expression.

  • When int appears in a type expression, it spells the type that represents the set of all instances of int (or subclasses thereof). As a value expression, int refers to the built-in class object named int, and its type is evaluated as type[int].

  • When ”bool” (with quotes) appears in a type expression, it spells a type that represents instances of the bool class. As a value expression, ”bool” is a str instance, and its type is Literal[“bool”].

  • When list[int] appears in a type expression, it spells a type that represents the set of all instances of list (or subclasses thereof) that have been specialized with a type argument of int. As a value expression, list[int] is an instance of GenericAlias, and its type is type[list[int]].

  • When int | str appears in a type expression, it spells a type that represents the union of the sets described by type expressions int and str. As a value expression, int | str is an instance of UnionType, and its type is UnionType.


The types used in the static type system are an abstraction. These abstract types should not be confused with the classes and objects that are used to implement type expressions and the other machinery of the type system at runtime. The runtime details of GenericAlias, UnionType, and ForwardRef are not part of the type abstraction. They don’t appear anywhere in the typing spec today, nor should they. They are implementation details that are hidden from users and not part of the mental model for static typing. The two concepts should not be confused.


Now, let’s take a deeper look at the proposal for TypeForm.

As I mentioned above, we recently made headway in defining the term “type expression” and specifying unambiguously where type expressions are used in the language. This clarity allows static type checkers to use type-expression evaluation rules in those cases and use value-expression evaluation rules in all other cases.

This draft proposal significantly muddies the waters, which I consider a big step backward. This draft says that any expression used in an assignment statement or as an argument in a call expression may need to be evaluated as either a type expression or a value expression — and perhaps both. And it doesn’t clearly explain the circumstances under which each of these evaluation rules should be used.

It has been suggested that a static type checker could use the inference context (the “expected type”) to determine whether type-expression evaluation rules should potentially be applied. For example, if a type checker encounters x: TypeForm = <expression>, it could first attempt to evaluate <expression> using type-expression evaluation rules (presumably suppressing any errors or warnings it encounters along the way). If that fails, it could then re-evaluate the same expression using value-expression evaluation rules. At first blush, this approach sounds reasonable, but it presents many problems. Here are a few of the issues I’ve identified so far.

  1. If errors are detected in both cases, which error message should be presented to the user? How should it be presented (which text range), and how should the message be worded to guide the user to a fix?
  2. What if the inference context is ambiguous, such as x: TypeForm | str = “list[int]”?
  3. How would this work for ternary expressions like x: TypeForm = int if condition else get_typeform() where int is a legal type expression but get_typeform() is not, even though it may return a value of type TypeForm? I’ll note that ternary expressions are not allowed for type expressions.
  4. What would a type checker do with this: x: TypeForm = list[reveal_type(“int”)]? It’s normally nonsensical to ask a static type checker to evaluate the type of a type expression (or a subexpression within a type expression), but here we’re blurring the lines between type and value expressions, so it’s not clear.

I’ll also note that the notion of an “inference context” (or “bidirectional type inference”) appears nowhere in the typing spec currently. Inference behaviors are generally left as details to each type checker. If this new feature relies on the notion of an “inference context”, we may need to incorporate some aspects of inference behavior into the spec.

I suspect the above list of issues will continue to grow as we further explore the current formulation. These may seem like edge cases that we can just sweep under the rug, but I find that edge cases are where we often get into the most trouble when implementing new type features.


The draft PEP says "A variable of type TypeForm[T] where T is a type, may only be assigned a class object or special form…”. There are a set of syntactic and semantic rules that a static type checker applies when evaluating a type expression. Are all of these same rules applied here? For example, can a variable be used in the expression? Can the expression contain a reference to an out-of-scope TypeVar? There are dozens of specific checks like this that apply to type expressions but not to value expressions. Do all of these checks apply here? Do we need to enumerate them all in the spec? Presumably so, since they would affect the type evaluation behavior.


The current draft indicates that this is allowed:

IntTreeRef: TypeForm = ForwardRef('IntTree')  # OK

This conflates the type abstraction used in type expressions and the internal implementation details used to implement type expressions at runtime, so this doesn’t make sense to me. The assigned expression is neither a valid type expression, nor is it a value expression that evaluates to TypeForm, so I don’t see how this could be allowed.


There has been some discussion about whether special forms that are considered “type qualifiers” should be allowed in a type form expression (e.g. Required[int] or Final[str]. I think the answer is clearly “no”. That can’t possibly work if we want TypeForm to be a generic class that accepts a type argument. First, there’s no way to spell these types since TypeForm[Final[str]] is not a legal type expression. Second, subtyping rules are not defined for type qualifiers, so it’s not clear whether TypeForm[Final[str]] is compatible with TypeForm[str], etc.


The issues that I discuss above lead me to think that we should take a step back and explore alternative solutions (or variants to the proposed solution) that avoid these problems but still address the use cases enumerated in this “motivations” section.

Here’s one idea for consideration. Maybe we should require that a TypeForm constructor be called.

x = TypeForm(int | str)
reveal_type(x)  # TypeForm[int | str]

y = TypeForm("Foo | Bar")
reveal_type(y)  # TypeForm[Foo | Bar]

This TypeForm call could then be added to the list of places where type expressions can appear. This would eliminate all of the ambiguities and would compose well with all existing type features. Admittedly, it makes the feature a bit more cumbersome to use, but maybe that’s a reasonable tradeoff.

I’m open to other ideas, but I have significant misgivings about the proposal as it’s currently framed.

8 Likes

Isn’t this a generalisation of TypeAlias? Is there a chance of undeprecating it for backwards compatibility? Type checkers would treat the 2 equivalently.

from typing import TypeAlias, TypeForm

Foo: TypeAlias = list[int]
Bar: TypeForm  = list[int]

That has the advantage that TypeAlias can be used to annotate type aliases starting from 3.10.
Currently there’s no (not deprecated) alternative.

Here’s what mypy does currently, a lot of the code seems to be already in place:

from typing import ForwardRef, TypeAlias

Foo: TypeAlias | int = list[int]  # OK
get_typeform = lambda: str

def foo(condition: bool) -> None:
    # Invalid type alias: expression is not a valid type
    x: TypeAlias = int if condition else get_typeform()

# Type expected within [...]
# Invalid type alias: expression is not a valid type
# The type "type[list[Any]]" is not generic and not indexable
# Revealed type is "Literal['int']?"
x: TypeAlias = list[reveal_type("int")]

# Invalid type alias: expression is not a valid type
IntTreeRef: TypeAlias = ForwardRef('IntTree')

There’s two issues.

  1. The primary value of TypeForm is when it appears as annotation of function argument. Using TypeForm as an annotation of variable where it’s similarish to TypeAlias is low value and none of the library use cases that benefit from TypeForm gain here. I still strongly lean towards being able to use TypeForm for variables is source of confusion and is almost never a good choice. Relatedly main use case for TypeForm instead of object is for TypeForm[T]. It is hard to find useful use cases for TypeForm without type variable which also does not align with type alias.
  2. TypeForm can fit in easily into typing_extensions so python version is not much of a concern.

My own leaning lies closer to supporting subscripting of generic functions + runtime access of function type arguments covers most of use cases for TypeForm and fits in with existing type system in more straight forward manner (at trade off that runtime implementation is bigger change).

If we take Mehdi’s suggestion, will it make the implementation easier?

Even without knowing these difficulties, I already tentatively agree with Mehdi: I don’t see why you would ever want to use TypeForm other than as TypeForm[T]. Only tentatively because David has a good point

But if the restriction eases the implemetation, that’s a good value, so should we give it another consideration?

For what it’s worth, I’ve been using pyright 1.1.345 for my personal projects because back then this shenanigan was allowed

def isassignable(value: object, typexpr: type[T]) -> TypeGuard[T]: ...
# type expressions could be assigned to type[T]
# TypeIs didn't exist yet
isassignable([1, "2"], list[int | str])

until it was fixed in 1.1.346 (rightfully so, but I happen to find this bug tremendously useful). If TypeForm can only be used as TypeForm[T], I think pyright 1.1.345 should already have most of the infrastructure required to implement this spec?

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(...).

Is there value in allowing TypeAlias as a replacement for TypeForm in older versions? (not requiring to install typing_extensions) It does something very similar (I think). It might make things easier for type checkers.


Could type statements be used for function parameters?

def trycast[T](type form[T], value: object) -> Optional[T]: ...

This would specify unambiguously that a type expressions is used here.

Yes, I watched the video. Thanks for creating that.

What you’re attempting to describe in the video (and above) are the different rules that a type checker uses when evaluating a type expression versus a value expression. Your list is incomplete, and even in its incomplete form, you have identified multiple cases that are ambiguous.

Even your first rule of recognizing “several literal forms” ignores the possibility that these special forms can be written in many ways (e.g. typing.Self or aliased in an import statement like from typing import Optional as Opt or imported from a different module that re-exports it as a different name). One can’t simply look at the literal form to disambiguate between a type expression and a value expression. One can look at the AST to rule out the possibility that it’s a valid type expression (e.g. if it contains a call expression), but that’s insufficient for disambiguating between type expressions and value expressions.

These complications are why we have worked so hard over the past six months to clarify precisely where type expressions appear in the language. Let’s not undo the progress we’ve made here.

Ah yes, I didn’t realize that you had created a separate document for draft 2. Apologies for my confusion.

I look forward to seeing draft 3. Once we get to a draft that provides a sound and unambiguous specification, I’d be happy to implement it in pyright with the goal of uncovering any unforeseen issues in the spec.

1 Like

@erictraut thanks for taking the time to detail several questions RE the “value expression + type expression mixing” issue.

TypeForm[T] values that already fit in type[T]

Conceptually TypeForm[T] and type[T] behave very similarly in my mind. Calling out some specific examples:

  • As a value expression, Movie (referring to a typed dict) has type type[Movie] according to pyright. mypy says something similar:

    reveal_type(Movie)
        # pyright: "type[Movie]"
        # mypy: "def (*, title: builtins.str) -> TypedDict('typeddict.Movie', {'title': builtins.str})"
    
    def identity_type(t: type[T]) -> type[T]:
        return t
    reveal_type(identity_type(Movie))
        # pyright: "type[Movie]"
        # mypy: "type[TypedDict('typeddict.Movie', {'title': builtins.str})]"
    

TypeForm[T] values that do not fit in type[T]

For those type expressions that, when parsed as a value expression, do not currently have type type[t] (for some t), I’d like them to ideally have type TypeForm[t] instead:

  • Stringified type expressions

    I think the user would have to explicitly mark string literals that are intended to be treated as TypeForms with the TypeForm(...) syntax you recently proposed.

    • Proposal: As a value expression, TypeForm("bool") (with quotes) has a type of TypeForm[bool].
    • Proposal: As a value expression, "bool" (with quotes) continues to have type Literal["bool"].
  • Unions

    Given each operand of an x | y expression, x and y, is itself a value expression with a type:

    • Proposal: If x has type type[t1] or TypeForm[t1], and y has type type[t2] or TypeForm[t2], then x | y has type TypeForm[t1 | t2]. Otherwise x | y has type UnionType.
  • Bare Forms

    Currently pyright and mypy disagree on the type of various special forms, with mypy mostly saying they are type object:

    reveal_type(Any)
      # mypy: "builtins.object"
      # pyright: "Any"
    reveal_type(Never)
      # mypy: "typing._SpecialForm"
      # pyright: "Never"
    reveal_type(Literal["words"])
      # mypy: "builtins.object"
      # pyright: "type[Literal['words']]"
    reveal_type(Callable[[], None])
      # mypy: "builtins.object"
      # pyright: "type[() -> None]"
    
    • Proposal: As a value expression, Any has a type TypeForm[Any].
    • Proposal: As a value expression, Never has a type TypeForm[Never].
    • etc
  • None

    Users writing None are almost certainly are intending to spell the None value rather than the None type. So in the rare cases that the None type is intended, users would need to use the TypeForm(None) syntax to spell it:

    • Proposal: As a value expression, TypeForm(None) has a type of TypeForm[None].
    • Proposal: As a value expression, None continues to have a type of None.

TypeForm(T) as an explicit way to spell an object T with type TypeForm[T]

I think it could be useful to have an explicit spelling of TypeForm(T) to mean “T when treated as a value”. I think this explicit spelling may actually be necessary to disambiguate a few cases (mentioned above).

At runtime the callable TypeForm() would just return its single argument unchanged. Type checkers could recognize TypeForm(t) and specially parse the t inside as a type expression, and give it type TypeForm[t].

Fin

Thoughts @erictraut ?

I like this idea. It requires that we standardize the type of special forms when they appear in value expressions. That’s never been specified in any PEPs or in the typing spec previously, but I think it’s OK to do so in the context of this PEP.

I’m not sure what we’d do with special forms that are not valid TypeForms, like Union (with no type arguments) or Required or ClassVar. Do we leave those undefined? Or should we take this opportunity to specify how all special forms should be evaluated by a type checker when used in a value expression? As you pointed out, there’s no consistency between type checkers currently, so there’s perhaps value in specifying this. On the other hand, one could argue that’s outside the scope of this PEP.

The only part of your proposal that I find problematic is “Otherwise x | y has type UnionType”. The good news is that I don’t think this case is one we need to be concerned with. If x and y are not legal TypeForms, then x | y will not be a legal UnionType. For example, 1 | 2 is a valid expression, but it doesn’t result in a UnionType instance. int | 2 is not a valid expression (it generates an exception at runtime). Same goes for int | "str". Can you think of any counterexamples?

One other point that I forgot to make in my previous post is that type[T] has a special property that is not currently documented in the typing spec (but probably should be): unions distribute across type. That is, type[A] | type[B] is equivalent to type[A | B]. This property doesn’t hold for most other covariant types (e.g. Sequence[int | str] is not the same as Sequence[int] | Sequence[str]). This special case for type is not documented, but all type checkers currently implement it.

def func(a: type[int | str], b: type[int] | type[str]):
    assert_type(a, type[int] | type[str])
    assert_type(b, type[int | str])

We should document this special case. I mention this in the context of this PEP because we may want to document that TypeForm has this same property.

Aye. I can enumerate in the next PEP draft rules for parsing each kind of type expression in a value expression context.

It would be useful to define that incomplete type expressions (like Union, Optional, Annotated - with no type arguments) should have type object. That would guarantee that you’d get a reasonable error message if you spelled something that is close to valid. Currently you see errors that look like:

def identity_type(t: type[T]) -> type[T]:
    return t

identity_type(Literal)
  # pyright: Argument of type "type[Literal]" cannot be assigned to parameter "t" of type "type[T@identity_type]"
  # mypy: Argument 1 to "identity_type" has incompatible type "<typing special form>"

Ideally annotation expressions which aren’t type expressions (like Required[...], Final, etc) should also be defined to have type object. I don’t think there’d be too many additional cases to write out so I think it would be OK to enumerate them.

There’s a corner case for Annotated[...] because it sometimes is a type expression and sometimes is an annotation expression:

  • Proposal: If x has type type[t] then Annotated[x, ...] has type type[t]
  • Proposal: If x has type TypeForm[t] then Annotated[x, ...] has type TypeForm[t]
  • Proposal: Otherwise Annotated[x, ...] has type object

Ah yes. I think I meant here to write something like “Otherwise x | y has the type implied by the binary-or bitwise-or of x and y”. For example the type of 5 | 2 would be int.

I agree that I don’t think there’s a case where you’d end up with UnionType here if the type and TypeForm cases were already checked.

Agreed. I’ll add a mention to the next PEP draft.

I thought of a few additional questions about your new idea.

First, what happens when a value expression consists of a special form but also includes a syntactic form that is not allowed in a type expression or uses a dynamic value (a variable), which is also not allowed in a type expression?

x = Literal[tuple(*["a", "b"])] # Syntactically invalid form
y = Union[var1, var2, var3] # Semantically invalid (uses variables)

I presume that these would not evaluate to valid TypeForm types. We’d need to decide what they evaluate to in that case. Maybe object is the right answer in all such cases?

Second, how does this interact with old-style type alias definitions (prior to the introduction of TypeAlias and long before the introduction of the type keyword)? These old-style type aliases have always been a pain because PEP 484 (and the typing spec) do not specify the exact rules for when a type checker should treat an assignment expression as a type alias or a regular variable assignment. Your idea has the potential to completely clear up this point. If the RHS evaluates to a TypeForm, then the assignment should be considered a type alias. If the RHS evaluates to anything other than a TypeForm, then the assignment should not be considered a type alias.

1 Like

It makes sense to me that it would evaluate to object in this case. Again that would continue to give reasonable errors when a type expression is almost but not quite valid:

LIST_OF_INT: TypeForm = list[int]  # OK

INT: TypeForm = int  # a variable
LIST_OF_INT2: TypeForm = list[INT]  # ERROR: Expected TypeForm but found object

Int: TypeAlias = int
LIST_OF_INT3: TypeForm = list[Int]  # OK

Another alternative I could see would be to both trigger an error (at type checking time) and evaluate to Any.


Draft 2 of the PEP currently says:

an unannotated variable assigned a TypeForm literal will not be inferred to be of TypeForm type by type checkers because PEP 484 reserves that syntax for defining type aliases:

STR_TYPE = str # OOPS; treated as a type alias!

If you want a type checker to recognize a TypeForm literal in a bare assignment you’ll need to explicitly declare the assignment-target as having TypeForm type:

STR_TYPE: TypeForm = str

It makes sense to me that if the RHS of an assignment evaluates to a TypeForm (or a type) and is unannotated that it be considered a type alias.

1 Like

I think there could be some mildly surprising behavior there, unless I misunderstand things. It would mean a very local error deeply nested in a very complex expression - potentially in a different module than the expression via an imported value - can silently change the nature of the expression, causing an error in some place other than the definition of the expression.

2 Likes