As far as I can tell from the documentation, the type annotations on f and g in the following code snippet should be equivalent:
from typing import TypeVar
import numpy as np
from numpy.typing import NDArray, NBitBase
W = TypeVar("W", bound=NBitBase)
Float = np.floating[W]
FloatVec = NDArray[Float]
def f(f: np.floating[W], v: NDArray[np.floating[W]]) -> NDArray[np.floating[W]]:
return v * f
def g(f: Float, v: FloatVec) -> FloatVec:
return v * f
The intention is to make Float shorthand for numpy.floating[W], and, thus, not a generic. But mypy objects:
$ mypy --strict test.py
test.py:7: error: Missing type parameters for generic type "Float" [type-arg]
test.py:12: error: Missing type parameters for generic type "Float" [type-arg]
Found 2 errors in 1 file (checked 1 source file)
Why does it think Float is still generic, and how do I make it mean what I wanted it to mean?
Float is a generic type alias, by virtue of it having a type variable in its definition.
They are not. In the given snippet, g is equivalent to
def g(f: np.floating[Any], v: NDArray[np.floating[Any]]) -> NDArray[np.floating[Any]]:
return v * f
If you want g to be generic, then you’ll having to fill in type variables into its signature, just as you did in f. For example:
W = TypeVar("W", bound=NBitBase)
Float = np.floating[W]
FloatVec = NDArray[Float[W]]
def g(f: Float[W], v: FloatVec[W]) -> FloatVec[W]:
return v * f
(note that the above also makes FloatVec a generic alias. In the original definition, FloatVec is an alias for the concrete type NDArray[Float[Any]], which doesn’t seem to be what you want).
There’s no way make a generic alias implicitly receive a certain type variable as its type argument, if that’s what you’re looking to do. Meaning, you can’t write something like
Foo = list[T]
def foo(x: Foo, v: T):
...
with the goal of having the type argument to Foo be the same type as the type of v. Here, Foo means Foo[Any].
You can check out the typing spec chapters on generics and type aliases for more details.
I think what you’re trying to say here is that the concrete value of W is ignored when defining a type alias, so, because np.floating is already a generic type, Float = np.floating[W] is effectively equivalent to Float = np.floating. Is that correct? I was not able to find anything in the documentation you linked to that speaks specifically about creating a type alias, from a generic type, with type variables appearing on the right-hand side of the definition.
These semantics are strange to me. You can do
IntList = list[int]
and then IntList means “a list specifically of integers”. So it seems to me that you ought to be able to do
and have that mean “a list of numbers, all of which are the same type, and that type is some concrete subtype of numbers.Number”. (“All of which are the same type” is the crucial difference from what you’d get with NumericList = list[numbers.Number].)
Why are the semantics of type aliases, with type variables appearing on the right-hand side of the definition, specified the way they are?
You know what, I’m not even sure I ought to be messing with NBitBase-bounded type variables at all. Here’s an actual example of a function I’m trying to write annotatIons for:
import numpy as np
def between(v, lo, hi):
return np.nonzero((v >= lo) & (v < hi))
Actual usage of this function is that v is an ndarray, of arbitrary shape and numeric dtype, and lo and hi have the same dtype as v (it’s fine if this is extremely strict, regardless of what numpy would accept at runtime). So this was my first attempt at that:
from typing import TypeVar
import numpy as np
from numpy.typing import NDArray
Num = TypeVar("Num", bound=np.number)
def between(v: NDArray[Num], lo: Num, hi: Num) -> tuple[NDArray[np.intp], ...]:
return np.nonzero((v >= lo) & (v < hi))
but mypy --strict doesn’t like it:
test.py:5: error: Missing type parameters for generic type "number" [type-arg]
Any suggestions for the ideal way to write this so that it passes mypy --strict with a minimum of additional ceremony, would probably get at my actual problem better than what we were talking about earlier.
It is not a concrete value. It is a type parameter in a generic type. A concrete type would be something like np.floating[int] not np.floating[W] (I’m not sure what numpy’s actual concrete types are called here).
With the Python 3.12 type parameter syntax (PEP 695) what you write here is impossible because you can only use a type parameter in the rhs if it also appears in the lhs:
type Float[W: NBitBase] = np.floating[W]
This makes it a bit clearer that the W is not a “concrete value” because you can’t have W as an independent object. It exists only to show how the type parameters in the two generic types are related.
This is perhaps the crux of the strangeness I’m experiencing.
In untyped Python, of courseW is a concrete value. It is a (module-)global variable referring to a concrete object, namely the object produced by evaluating TypeVar("W", bound=NBitBase). And of courseFloat = np.floating[W] should produce another (module-)global variable which is the result of evaluating np.floating[W], and of course using Floatanywhere should have the same semantics as using np.floating[W] in that location, modulo object identity.
You and @bschubert are saying that it doesn’t work like that, because the objects involved in constructing type annotations have special semantics. I get that. But do you get that these special semantics are weird to someone with a lot of experience with untyped Python but relatively new to typed Python?
The 3.12 syntax makes type parameters less confusing I think. From memory the PEP has some discussion about this.
At runtime they do have the same semantics but the static type checker does not run the code (and I don’t recommend actually using these objects at runtime). Instead type checkers recognise certain particular assignments as defining type aliases.
In Python 3.12 onwards the expected way to declare a type alias is to use the type statement that I showed before which clearly shows it as something different from a normal assignment and does not behave like a normal assignment at runtime either.