The AstroPy library documents a suggested pattern to type-annotate using an expression like Quantity[u.km]. Quantity is not a generic class, but it implements __class_getitem__ to return Annotated[Quantity, u.km] – effectively a shorthand for an Annotated annotation.
AstroPy itself does not type-annotate the return value of Quantity.__class_getitem__ and doesn’t seem to publish type stubs, but recently a someone inquired about how to write type stubs for this pattern that will work in ty.
ty requires a class subscripted in a type expression to be defined as generic (that is, have PEP 695 type variables, or inherit typing.Generic, or typing.Protocol with type variables), otherwise it emits invalid-type-form. Mypy and zuban similarly error on this example:
from __future__ import annotations
class U:
def __class_getitem__(self, value: int) -> type[U]:
return U
def _(x: U[0]): # error, can't subscript a non-generic class in a type expression
reveal_type(x)
Pyright and pyrefly allow this without error and reveal U on the last line, though their support differs in scope. Pyright always reveals U on the last line, even if the __class_getitem__ is modified to return type[int] – it doesn’t actually respect the return type. Pyrefly reveals int instead with that return type change.
My reading of the current spec language is that it strongly suggests that this pattern should not be allowed. The type expression grammar is explicitly restrictive, and makes no allowance for subscripts in type expressions to be something that is not itself a type expression. 0 in the above example is clearly not a type expression.
I think the main risk in explicitly allowing this is that it opens the door for confusing/problematic mismatches between runtime behavior and what the type checker can understand. For example, consider this code:
class Factory:
def __class_getitem__(cls, key: int) -> type[object]:
return int
x: Factory[0] = "hello"
At runtime, Factory.__class_getitem__ returns int, but since type is covariant, it can be annotated as returning type[object]. So the annotation actually evaluates to int (and runtime annotation introspection will see int), but the type checker can only see object, and will allow the assignment of "hello".
I would be interested in hearing especially from pyrefly and pyright maintainers about your reasons for permitting this, and from anyone with opinions about whether this should be allowed or not. I think it would be useful to clarify and specify this one way or the other. It would not be hard for ty to allow this, if we decide that’s the right path. I assume the __class_getitem__ would be required to return a valid TypeForm (which type[...] is), or else it should be an error? (Though currently pyright and pyrefly are also fine with no return annotation at all, in which case they both infer the subscripted class type.)