@davidfstr, thanks for continuing to push this idea forward!
I’ve reviewed the latest draft in its entirety. I think it makes good progress in some areas, but it also (IMO) takes some steps backward in other areas.
Type expression terminology
The PEP continues to confuse the meaning of the term “type expression”. It sometimes uses the term properly to describe “an expression that ’spells’ a type”, but it often conflates this with “the type that is spelled by a type expression”. Using the same term for both concepts is causing confusion in the PEP and in the discussions about the PEP.
Let’s be clear about what an “expression” is. It’s a grammatical construct that represents a tree of operations and operands. When the Python runtime evaluates an expression, it produces a value. When a static type checker evaluates an expression, it produces a type that represents all possible values that can be produced by that expression at runtime. In neither case (runtime nor type checker) does the evaluation of an expression produce another expression. It would be confusing, for example, to say that the expression (x ** 2.0) * PI has the type “float expression”. Its type is simply “float”. Likewise, if we evaluate a “type expression”, we wouldn’t say that the evaluated type is a “type expression”. It evaluates to a type in the Python type system.
The proposed name TypeExpr further serves to confuse these two concepts, so I think that using TypeExpr here is a big step backward over the previous TypeForm. I’m not wedded to the term TypeForm, but I think it’s much better than TypeExpr. If people don’t like TypeForm, let’s look for some other option that better describes “a type in the Python type system”. I’ll throw out a few here: TypeObject or SpelledType. (Of these options, my personal preference is still TypeForm.)
I apologize if this comes across as bikeshedding, but terminology matters. And in this case, I think TypeExpr is not serving us well.
TypeExpr without brackets
The latest draft says:
It can also be written without brackets as just TypeExpr, in which case a type checker should apply its usual type inference mechanisms to determine the type of its argument, possibly Any.
This is inconsistent with all other type expressions that take type arguments, and it will not compose well with other type features. If a type argument is omitted from a type, type checkers should always assume Any. For comparison, the type type is interpreted as type[Any]. The type list is interpreted as list[Any]. Likewise, the type TypeExpr should be interpreted as TypeExpr[Any]. There should be no exception. There is no need to apply inference rules here.
Rules for unions
In the “Implicit TypeExpr Values” section, there’s a subsection that discusses the rules for unions. It says:
As a value expression, x | y has type TypeExpr[x | y] if x has type TypeExpr[t1] (or type[t1]) and y has type TypeExpr[t2] (or type[t2]).
This is problematic for classes whose metaclass overrides the __or__ or __ror__ method. In this case, the type of value expression x | y should honor the method on the metaclass rather than assume that the expression is intended as a type expression.
Current type checkers evaluate the value expression x | y as type UnionType (unless the metaclass overrides __or__ or __ror__). Some typeshed definitions have come to rely on this UnionType behavior. Most notably, the definition for isinstance and issubclass use UnionType in their signature. That’s because these two calls (unfortunately, IMO) accept value expressions of the form x | y. It’s not clear to me how the signatures for these two functions would change given the proposal in this PEP. I’m concerned that there will be no way to express these signatures accurately if we switch to the rules proposed in this draft PEP. More generally, I recommend looking at all the places where typeshed stubs currently use UnionType and asking whether the proposal in this PEP breaks those usages.
Rules for Annotation Expressions
The latest draft includes a section titled “Implicit Annotation Expression Values”. I strongly recommend deleting this section. The idea of representing runtime types of annotation expressions is not well motivated in this PEP. If someone were to ever propose adding such a construct to the type system, I would push back hard against it. These constructs do not spell types, so the result of evaluating one of these as a value expression will not follow normal type calculus rules. What would it mean to take the union of Final[str] and ClassVar[int]? What would it mean to take the intersection of Required[str] and Unpack[tuple[()]]? Is Final[str] a subtype of str? These are nonsensical questions. This construct doesn’t belong in the type system. Including this section in the PEP is confusing, problematic, and unnecessary.
I likewise recommend removing any mention of the “Annotation expression object” from the “Rationale” section.
Subtyping Rules
One of the rules in the “Subtyping” section uses the term “plain type”. I think I know what you mean by this, but it isn’t a term that used anywhere else in the typing spec, so it could be misinterpreted. I recommend deleting the rule that uses this term. It’s unnecessary if TypeExpr (with no type arguments) is always interpreted as TypeExpr[Any], as it should be.
Literal[] TypeExprs
The “Literal[] TypeExprs" section begins: “To simplify static type checking…”. I don’t think that’s an accurate or well-reasoned justification for the rule that follows. The reason this shouldn’t be supported is that variables (dynamic values) are not allowed in type expressions. In your example, STRS_TYPE_NAME is a variable, and the fact that it appears in the expression means it is not a valid type expression.