# Should non-generic classes with \`\_\_class\_getitem\_\_\` be subscriptable in a type expression?

**URL:** <https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666>\
**Category:** Typing\
**Created:** [August 20, 2026, 4:18pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666 "2026-08-20T16:18:43Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![carljm](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/carljm/32/959_2.png) [@carljm](https://discuss.python.org/u/carljm)\
**Post date:** [August 20, 2026, 4:18pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/1 "2026-08-20T16:18:43Z")

</div>

The AstroPy library [documents a suggested pattern](https://docs.astropy.org/en/latest/units/type_hints.html#preserving-units) 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](https://github.com/astral-sh/ty/issues/4331) 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:

```py
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](https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions) 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:

```py
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.)

---

<div class="post-metadata">

**Author:** ![mikeshardmind](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/mikeshardmind/32/14381_2.png) [@mikeshardmind](https://discuss.python.org/u/mikeshardmind)\
**Post date:** [August 20, 2026, 5:42pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/2 "2026-08-20T17:42:12Z")

</div>

I would say it should be allowed, and that the return type annotated should be used, no guessing (that means using Any in the absence of an annotated return type, not assuming generic behavior)

I’m not concerned about the potentially confusing outcomes related to covariance of type, people manually implementing ` __class_getitem__ ` rather than relying on the default generic semantics are already in advanced usage territory.

---

<div class="post-metadata">

**Author:** ![rchen152](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/rchen152/32/16955_2.png) [@rchen152](https://discuss.python.org/u/rchen152)\
**Post date:** [August 21, 2026, 9:02pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/3 "2026-08-21T21:02:07Z")

</div>

I took a quick look at the history of ` __class_getitem__ ` support in Pyrefly, and it doesn’t look like we had much discussion on the topic. The relevant issues and PR are:

- [Support ` __class_getitem__ ` · Issue #256 · facebook/pyrefly · GitHub](https://github.com/facebook/pyrefly/issues/256)
- [Subscripting on classes should attempt to invoke dunder methods · Issue #259 · facebook/pyrefly · GitHub](https://github.com/facebook/pyrefly/issues/259)
- [fix Support ` __class_getitem__ ` by asukaminato0721 · Pull Request #1349 · facebook/pyrefly · GitHub](https://github.com/facebook/pyrefly/pull/1349)

Basically, we started from the assumption that we ought to support ` __class_getitem__ ` because it works at runtime and proceeded accordingly.

IMO using ` __class_getitem__ ` as basically a way to get static typing to compose with other use cases for annotations, as AstroPy seems to be doing, is reasonable, and I’d be in favor of allowing it.

---

<div class="post-metadata">

**Author:** ![Jelle](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/jelle/32/1049_2.png) [@Jelle](https://discuss.python.org/u/Jelle)\
**Post date:** [August 22, 2026, 1:52am UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/4 "2026-08-22T01:52:44Z")

</div>

I also think it makes sense to allow this, as long as the ` __class_getitem__ ` returns some kind of TypeForm. For example, in this variant:

```python
from __future__ import annotations
from typing_extensions import TypeForm

class U:
    def __class_getitem__ (self, value: int) -> TypeForm[int]:
        return int

def _(x: U[0]):
    reveal_type(x)

```

I’d expect to reveal `int` (as pycroscope does, though I can’t say I consciously designed for it).

As @mikeshardmind suggested we can use Any if the ` __class_getitem__ ` is unannotated. Using one that returns a non-TypeForm should be an error (e.g., if it is annotated as `-> int`).

---

<div class="post-metadata">

**Author:** ![carljm](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/carljm/32/959_2.png) [@carljm](https://discuss.python.org/u/carljm)\
**Post date:** [August 22, 2026, 5:22am UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/5 "2026-08-22T05:22:47Z")

</div>

Thanks all! Sounds like the balance of opinion is to allow this.

In considering it further, my hesitation is that this weakens the distinction between type and value expressions, and allows arbitrary dynamic code to determine the type spelled by a type expression.

Should there be any limits on what code can appear inside the subscript in this case? Are arbitrary Python value expressions valid there? Including e.g. calls (normally banned in type expressions), other subscripts (of arbitrary objects, not necessarily classes with ` __class_getitem__ `), etc? Or should there be some limits? If so, what should the limits be? (As far as I can tell, pyright and pyrefly do not apply any limits.)

We already support arbitrary dynamic Python expressions in value expressions, via `Annotated` – but that feels a bit different, since a type checker does not even need to evaluate those expressions, it can ignore them. This would be the first time (AFAICT) that we allow arbitrary dynamic code in a Python type expression to determine the type spelled by that type expression. (Pyright’s version of “supporting” this does not actually do that, but pyrefly’s does, and that’s the one it sounds like people would prefer.)

This makes me a bit uncomfortable. I’d have to say that my preference would be not to open this door.

---

<div class="post-metadata">

**Author:** ![dr\_carlos](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/dr_carlos/32/30461_2.png) [@dr\_carlos](https://discuss.python.org/u/dr_carlos)\
**Post date:** [August 22, 2026, 7:28am UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/6 "2026-08-22T07:28:42Z")

</div>

> [@carljm](#):
>
> we allow arbitrary dynamic code in a Python type expression to determine the type spelled by that type expression

My understanding was that whilst this might allow a dynamic object to be used as a type, since the return type of ` __class_getitem__ ` has to be declared statically, it’s not really making it more dynamic?

Though I guess that might cause different behaviour between static type checkers which don’t narrow the ` __class_getitem__ ` return type based on the code inside, and dynamic ones which do.

---

<div class="post-metadata">

**Author:** ![dangotbanned](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/dangotbanned/32/25910_2.png) [@dangotbanned](https://discuss.python.org/u/dangotbanned)\
**Post date:** [August 22, 2026, 11:08am UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/7 "2026-08-22T11:08:37Z")

</div>

> [@carljm](#):
>
> `Quantity` is not a generic class, but it implements ` __class_getitem__ ` to return `Annotated[Quantity, u.km]` – effectively a shorthand for an `Annotated` annotation.

[`Quantity`](https://github.com/astropy/astropy/blob/4bd86a680fee2980b5cf95dec5827ed2eb7b6dba/astropy/units/quantity.py#L291-L357) is a subclass of [`numpy.ndarray`](https://github.com/numpy/numpy/blob/1f90bbb55e65e7de68921bc3f1a2d94492fdf689/numpy/ __init__.pyi#L2210-L2211) which is a subclass of `Generic` _in the stubs_, but not at runtime:

```py
>>> import numpy as np
>>> np.ndarray.mro()
[<class 'numpy.ndarray'>, <class 'object'>]

```

IIUC, a type checker would consider `Quantity` as `Generic` and have inherited [2 type parameters with defaults](https://github.com/numpy/numpy/blob/1f90bbb55e65e7de68921bc3f1a2d94492fdf689/numpy/ __init__.pyi#L813-L814).

> [@carljm](#):
>
> The AstroPy library [documents a suggested pattern](https://docs.astropy.org/en/latest/units/type_hints.html#preserving-units) to type-annotate using an expression like `Quantity[u.km]`

**Question**  
If `u.km` is not assignable to [`tuple[int, ...]`](https://github.com/numpy/numpy/blob/1f90bbb55e65e7de68921bc3f1a2d94492fdf689/numpy/_typing/_shape.py#L4) \[1\], is a type checker expected to ignore the issue because of a ` __class_getitem__ `?

* * *

The [runtime implementation](https://github.com/astropy/astropy/blob/4bd86a680fee2980b5cf95dec5827ed2eb7b6dba/astropy/units/quantity.py#L434-L451) makes me think they’d be better served by ([PEP 835: Shorthand syntax for Annotated type metadata](https://discuss.python.org/t/pep-835-shorthand-syntax-for-annotated-type-metadata/107814)).

Using [preserving-units](https://docs.astropy.org/en/latest/units/type_hints.html#preserving-units) as an example, something like this possible _today_ and not much of a mouthful \[2\] if you alias some symbols:

> **Show a few generics**
>
> ```py
> from typing import Annotated as A, Literal as L
> 
> import numpy as np
> 
> class Quantity(np.ndarray): ...
> 
> class Unit[T]:
> def __init__ (self, arg: T) -> None:
> self.arg: T = arg
> 
> class PhysicalType[T]:
> def __init__ (self, arg: T) -> None:
> self.arg: T = arg
> 
> type m = Unit[L["m"]]
> type length = PhysicalType[L["length"]]
> 
> ```
> 
> ```py
> >>> A[Quantity, m]
> typing.Annotated[__main__.Quantity, m]
> 
> >>> A[Quantity, length]
> typing.Annotated[__main__.Quantity, length]
> 
> ```

* * *

1. [`_ShapeT_co`](https://github.com/numpy/numpy/blob/1f90bbb55e65e7de68921bc3f1a2d94492fdf689/numpy/ __init__.pyi#L813)’s bound 

2. which seems like the goal for defining ` __class_getitem__ ` in the first place

---

<div class="post-metadata">

**Author:** ![jorenham](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/jorenham/32/27806_2.png) [@jorenham](https://discuss.python.org/u/jorenham)\
**Post date:** [August 22, 2026, 11:18am UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/8 "2026-08-22T11:18:13Z")

</div>

> [@dangotbanned](#):
>
> [`Quantity`](https://github.com/astropy/astropy/blob/4bd86a680fee2980b5cf95dec5827ed2eb7b6dba/astropy/units/quantity.py#L291-L357) is a subclass of [`numpy.ndarray`](https://github.com/numpy/numpy/blob/1f90bbb55e65e7de68921bc3f1a2d94492fdf689/numpy/ __init__.pyi#L2210-L2211) which is a subclass of `Generic` _in the stubs_, but not at runtime:
> 
> ```python
> >>> import numpy as np
> >>> np.ndarray.mro()
> [<class 'numpy.ndarray'>, <class 'object'>]
> 
> ```

That’s because `numpy.ndarray` is defined in C. It does, however, implement ` __class_getitem__ `, which returns a `types.GenericAlias`:

```pycon
>>> import numpy as np
>>> np.ndarray[tuple[int], np.dtype[np.float64]]
numpy.ndarray[tuple[int], numpy.dtype[numpy.float64]]

```

So for all intents and purposes it’s a full-fledged generic type, both at runtime and in the stubs.

But that doesn’t mean that this should also be the case for _all_ of its subtypes. For example, `class MyVec(np.ndarray[tuple[int], np.dtype[np.float64]]): ...` is not a generic type.

---

<div class="post-metadata">

**Author:** ![dangotbanned](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/dangotbanned/32/25910_2.png) [@dangotbanned](https://discuss.python.org/u/dangotbanned)\
**Post date:** [August 22, 2026, 11:29am UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/9 "2026-08-22T11:29:45Z")

</div>

> [@jorenham](#):
>
> But that doesn’t mean that this should also be the case for _all_ of its subtypes.

Ah I just came back to make an edit that I’d confused myself with this part

> <https://github.com/astropy/astropy/blob/4bd86a680fee2980b5cf95dec5827ed2eb7b6dba/astropy/units/quantity.py#L426-L432>

But you’ve beaten me to it 😉

Perhaps the `ndarray` part isn’t relevant _because of_ the “inherited” defaults

---

<div class="post-metadata">

**Author:** ![mikeshardmind](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/mikeshardmind/32/14381_2.png) [@mikeshardmind](https://discuss.python.org/u/mikeshardmind)\
**Post date:** [August 22, 2026, 12:33pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/10 "2026-08-22T12:33:13Z")

</div>

> [@carljm](#):
>
> This would be the first time (AFAICT) that we allow arbitrary dynamic code in a Python type expression to determine the type spelled by that type expression.

I wouldn’t support it if it required this, but as @dr_carlos said, using the declared return type of ` __class_getitem__ ` prevents it from being arbitrary dynamic code that typecheckers are forced to evaluate, it still has to follow the normal semantics of function return type annotations.

specifying/requiring inference behavior is something I ruled out, stating that it should use Any in the absence of an annotation. A specific type checker applying inference behavior here is still fine if it only does it in local inference, not for library code, but really not ideal IMO

---

<div class="post-metadata">

**Author:** ![carljm](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/carljm/32/959_2.png) [@carljm](https://discuss.python.org/u/carljm)\
**Post date:** [August 22, 2026, 2:59pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/11 "2026-08-22T14:59:13Z")

</div>

@dr_carlos @mikeshardmind This proposal does allow arbitrary dynamic value expression evaluation to determine the meaning of the type expression, unless we _also_ add an explicit rule forbidding ` __class_getitem__ ` from being generic or overloaded.

Pyrefly accepts this code, and conditionally interprets the type of `x` as `int` or `str` depending which overload is selected:

```py
from typing import overload

class C:
    @overload
    def __class_getitem__ (cls, o: int) -> type[int]: ...
    @overload
    def __class_getitem__ (cls, o: str) -> type[str]: ...
    
    def __class_getitem__ (cls, o: object) -> type[object]:
        return type(o)
    
def returns_int() -> int:
    return 1
    
def _(x: C[returns_int()]):
    reveal_type(x)

```

---

<div class="post-metadata">

**Author:** ![mikeshardmind](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/mikeshardmind/32/14381_2.png) [@mikeshardmind](https://discuss.python.org/u/mikeshardmind)\
**Post date:** [August 22, 2026, 3:07pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/12 "2026-08-22T15:07:43Z")

</div>

I don’t see an issue with this example, the intended type can be evaluated statically using only the annotations. `returns_int` doesn’t need to be executed here; this isn’t arbitrary execution.

---

<div class="post-metadata">

**Author:** ![carljm](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/carljm/32/959_2.png) [@carljm](https://discuss.python.org/u/carljm)\
**Post date:** [August 22, 2026, 3:46pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/13 "2026-08-22T15:46:47Z")

</div>

I’m not claiming that _actual Python runtime execution_ would be required; obviously not. But this requires _type evaluation of arbitrarily complex Python value expressions_ in order to understand the meaning of a type annotation. This is something the Python type system has, until now, explicitly avoided. It means that the type expression grammar is no longer sufficient to understand the meaning type expressions.

It’s certainly _possible_ for type checkers to do this, but it has consequences both for type checker architecture and for the human comprehensibility of type annotations as a _static_ type system. I don’t think it’s a bridge we should cross without careful consideration.

---

<div class="post-metadata">

**Author:** ![mikeshardmind](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/mikeshardmind/32/14381_2.png) [@mikeshardmind](https://discuss.python.org/u/mikeshardmind)\
**Post date:** [August 22, 2026, 4:03pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/14 "2026-08-22T16:03:52Z")

</div>

I think the use cases for this are pretty clear, and it’s an obvious extension of what type checkers already understand.

I think any “this is hard for users to understand” argument comes down to “library authors should be responsible with this to cases that are naturally understood”. The case prompting the discussion is naturally understood.

When it comes to tooling complexity/architecture, tooling is _for the benefit of users_, the use here is clearly a more ergonomic composition for users, and if we determine that because some tools might not want the complexity, users should have a worse experience, I think we’ve lost sight of the fact that the complexity for tools is precisely to create a better experience for users.

---

<div class="post-metadata">

**Author:** ![davidhalter](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/davidhalter/32/2935_2.png) [@davidhalter](https://discuss.python.org/u/davidhalter)\
**Post date:** [August 22, 2026, 4:59pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/15 "2026-08-22T16:59:46Z")

</div>

I’m pretty strongly against mixing inference and static type definitions and I don’t like the fact that this works in Pyrefly. I used to allow more dynamic type definitions in Zuban with type inference and it was a really bad idea, because there can be very weird cycles that are hard to understand for both users and type checkers (lots of bugs).

I can kind of see us allowing non-generic TypeForms as a return of ` __class_getitem__ `, where we can ignore the expression like in `Annotated`, but I’m also not a fan of that and think that it’s a bad idea. I also think that `x: C[returns_int()]` looks really bad.

---

<div class="post-metadata">

**Author:** ![Liz](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/liz/32/15021_2.png) [@Liz](https://discuss.python.org/u/Liz)\
**Post date:** [August 22, 2026, 5:25pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/16 "2026-08-22T17:25:07Z")

</div>

I’d just keep the restriction on not having function calls in type expressions, while allowing the annotation on ` __class_getitem__ ` to declare the actual resulting TypeForm. I agree with @mikeshardmind about this being something that library authors should be responsible about, and also with what @Jelle wrote about still restricting the result to type forms.

I have no issue with AstroPy’s use, the part that goes beyond what I think should be allowed in your latest example is allowing the function call.

Even with function calls and only using the annotation in the type system, this would be tamer than other open proposals about type transformations on the effect of:

> [@carljm](#):
>
> It means that the type expression grammar is no longer sufficient to understand the meaning type expressions.

though the grammar was also never sufficient alone to understand the meaning of a type expression. Some symbols have their own rules, and they aren’t keywords encoded into the grammar.

---

<div class="post-metadata">

**Author:** ![jorenham](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/jorenham/32/27806_2.png) [@jorenham](https://discuss.python.org/u/jorenham)\
**Post date:** [August 22, 2026, 7:43pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/17 "2026-08-22T19:43:45Z")

</div>

Numba uses a pretty neat syntax for declaring N-dimensional arrays of certain dtype when specifying the signature of a `@jit` function, where you write e.g. `float32[:]` for a one-dimensional single-precision array, or `int8[:, :]` for a two-dimensional array of 8-bit integers ([docs](https://numba.readthedocs.io/en/stable/user/jit.html#signature-specifications)). I first thought that this might be a good use-case for allowing non-generic ` __class_getitem__ `, but I then realized that these numba types like `numba.float32` are instances rather than types, so they can just use ` __getitem__ `.

However, for a long time I have wondered whether it would be somehow possible to also use a similar terse syntax in in NumPy, i.e. being able to write `np.int8[:, :]` instead of the painfully verbose `np.ndarray[tuple[int, int], np.dtype[np.float32]]` \[1\]. But this always seemed unrealistic to me, and thought that it would require numpy-specific special-casing to be accepted as part of the typing spec.  
This thread made me realize that this is actually not as far-fetched as I initially thought. Because by allowing ` __class_getitem__ ` to also be used to return `TypeForm`s that are allowed to be used in annotations, then we’d effectively be able to define custom dependent type constructors. For the shaped dtype constructors in NumPy, that could look something like this:

```py
type Dim = slice[None, None, None]
type Float32ND[ShapeT: tuple[int, ...]] = ndarray[ShapeT, dtype[float32]]

class float32:
    # --snip--

    @overload
    def __class_getitem__ (cls, key: tuple[()], /) -> TypeForm[Float32ND[tuple[()]]: ...
    @overload
    def __class_getitem__ (cls, key: Dim, /) -> TypeForm[Float32ND[tuple[int]]: ...
    @overload
    def __class_getitem__ (cls, key: tuple[Dim, Dim], /) -> TypeForm[Float32ND[tuple[int, int]]: ...
    @overload
    def __class_getitem__ (cls, key: tuple[Dim, Dim, Dim], /) -> TypeForm[Float32ND[tuple[int, int, int]]: ...

    # --snip--

    @overload
    def __class_getitem__ (cls, key: tuple[Dim, ...], /) -> TypeForm[Float32ND[tuple[Any, ...]]: ...

```

I realize that this wouldn’t be easy to implement in type-checkers, to say the least. But it would make for an immensely powerful addition to Python’s type system, as it will allow us to overload _types themselves_. It would also be direct solution to [What would it take to implement type mapping for generics?](https://discuss.python.org/t/what-would-it-take-to-implement-type-mapping-for-generics/83402) without requiring any language changes whatsoever.

Clearly, this ` __class_getitem__ ` + `TypeForm` stuff will require more research and discussion before going forward with it. So for now let’s just consider it a “potential use-case” that allowing non-generic ` __class_getitem__ ` definitions will enable. Then after that, we could discuss this `TypeForm`-driven dependent type constructor stuff separately.

And to be clear: it’s not my intention for this to be a serious proposal, just a potential one. I’m only bringing it up _here_ because it requires us to allow ` __class_getitem__ ` in non-generic-type contexts. Then, if we decide on this, I’ll open a thread where we can discuss this in more detail \[2\].

* * *

1. This could be made less painful using type aliases, but then you’d end up with a huge number of type aliases (one for each combination of dtype and rank of up to 3 or 4 or something), which clearly also isn’t ideal. 

2. That is, unless it turns out not to be feasible, after all.

---

<div class="post-metadata">

**Author:** ![rchen152](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/rchen152/32/16955_2.png) [@rchen152](https://discuss.python.org/u/rchen152)\
**Post date:** [August 22, 2026, 8:02pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/18 "2026-08-22T20:02:04Z")

</div>

I started poking around at what can be done with `TypeForm` today, and I realized that mypy, pyright, ty, and zuban all error on this:

```python
from typing_extensions import TypeForm
X: TypeForm[int] = int
x: X # Error: variable not allowed in type expression
x = 0

```

(Pyrefly also errors but on the _next_ line, which seems like a gap in our implementation of `TypeForm` rather than any deliberate decision.)

So now I’m curious: is this restriction on the use of `TypeForm` intentional? For me, I think that changes whether this use of ` __class_getitem__ ` (with some limitations on what’s accepted and returned) feels like a natural extension of existing capabilities or opening the door to something totally new.

---

<div class="post-metadata">

**Author:** ![mikeshardmind](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/mikeshardmind/32/14381_2.png) [@mikeshardmind](https://discuss.python.org/u/mikeshardmind)\
**Post date:** [August 22, 2026, 8:08pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/19 "2026-08-22T20:08:58Z")

</div>

pyright presents the same error on the following, and the error predates TypeForm being accepted. The error message also explicitly states it is about variable use in a type expression, which the propsed relaxation on ` __class_getitem__ ` doesn’t require relaxing.

```python
X: type[int] = int
x: X
x = 0

```

---

<div class="post-metadata">

**Author:** ![rchen152](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/rchen152/32/16955_2.png) [@rchen152](https://discuss.python.org/u/rchen152)\
**Post date:** [August 22, 2026, 11:57pm UTC](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666/20 "2026-08-22T23:57:38Z")

</div>

Yes, the fact that the error predates `TypeForm` is why I’m wondering whether it was extended to `TypeForm` intentionally or essentially by historic accident.

I was trying to figure out how novel it would be to say that a value annotated as `TypeForm` can be used as a type by checking if `TypeForm` already works this way anywhere else.

[Next page](https://discuss.python.org/t/should-non-generic-classes-with-class-getitem-be-subscriptable-in-a-type-expression/108666.md?page=2)
