The documentation for `np.typing.NDArray`

says that it is "a generic version of `np.ndarray[Any, np.dtype[+ScalarType]]`

". Where is the generalization in "generic" happening?

And in the documentation for `numpy.ndarray.__class_getitem__`

we have this example `np.ndarray[Any, np.dtype[Any]]`

with no explanation as to what the two arguments are.

And why can I do `np.ndarray[float]`

, ie just use one argument? What does that mean?

Answers(1) :

"Generic" in this context means "generic type", typing-related objects that can be subscripted to generate more specific type "instances" (apologies for the sloppy jargon, I'm not well-versed in typing talk). Think `typing.List`

that lets you use `List[int]`

to denote a homogeneous list of ints.

As of Python 3.9 most standard-library collections have been upgraded to be compatible with typing as generic types themselves. Since `tuple[foo]`

used to be invalid until 3.9, it was safe to allow `tuple[int, int]`

to mean the same thing that `typing.Tuple[int, int]`

used to mean: a tuple of two integers.

So as of 3.9 NumPy also allows using the `np.ndarray`

type as a generic, this is what `np.ndarray[Any, np.dtype[Any]]`

does. This "signature" matches the actual signature of `np.ndarray.__init__()`

(`__new__()`

if we want to be correct):

```
class numpy.ndarray(shape, dtype=float, ...)
```

So what `np.ndarray[foo, bar]`

does is create a type for type hinting that means "a NumPy array of shape type `foo`

and dtype `bar`

". People normally don't call `np.ndarray()`

directly anyway (rather using helpers such as `np.array()`

or `np.full_like()`

and the like), so this is doubly fine in NumPy.

Now, since most code runs with arrays of more than one possible number of dimensions, it would be a pain to have to specify an arbitrary number of lengths for the shape tuple (the first "argument" of `np.ndarray`

as a generic type). I assume this was the motivation to define a type alias that is still a generic in the *second* "argument". This is `np.typing.NDArray`

.

It lets you easily type hint something as an array of a given type without having to say anything about the shape, covering a vast subset of use cases (which would otherwise use `np.ndarray[typing.Any, ...]`

). And this is still a generic, since you can parameterise it with a dtype. To quote the docs:

```
>>> print(npt.NDArray)
numpy.ndarray[typing.Any, numpy.dtype[+ScalarType]]
>>> print(npt.NDArray[np.float64])
numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]
```

As usual with generics, you're allowed to specify an argument to the generic type, but you're not required to. `ScalarType`

is derived from `np.generic`

, a base class that covers most (maybe all) NumPy scalar types. And the library code that defines `NDArray`

is here, and is fairly transparent to the point of calling the helper `_GenericAlias`

(apparently a backport of `typing.GenericAlias`

). What you have at the end is a type alias that is still generic in one variable.