Repository navigation
Description
NumPy 2.2 had a lot of typing improvements, but that also means some regressions (at least and maybe especially for mypy users).
So maybe this exercise is mainly useful to me to make sense of the mega-issue in gh-27957.
My own take-away is that we need the user documentation (gh-28077), not just for users, but also to understand better who and why people have to change their typing. That is to understand the two points:
- How many users and what kind of users are affected:
- Early "shaping users" of unsupported shapes may be few?
mypyusers of unannotated code are maybe quite many.
- And what do they need to do:
- Removing shape types seems easy (if unfortunate).
- Adding
--allow-redefinitionis easy, avoidingmypymay be more work (maybe unavoidable). - Are there other work-around? Maybe
scipy-lecturesis "special" or could hide generic types outside the code users see...
One other thing that I would really like to see is also the "alternatives". Maybe there are none, but I would at least like to spell it out, as in:
Due to ... only thing that we might be able to avoid these regression is to hide it away as from numpy.typing_future import ndarray and that is impractical/impossible because...
CC @jorenham although it is probably boring to you, also please feel free to amend or expand.
Issues that require user action
User issues due to (necessarily) incomplete typing
There are two things that came up where NumPy used to have less precise or wrong typing, but correcting it making it more precise (while also necessarily incomplete as it may require a new PEP) means that type checking can fail:
floatingis now used as a supertype offloat64(rather than identity) meaning it (correctly) matchesfloat32,float, etc.- Incomplete typing means functions may return
floatingrather thanfloat64even when they clearly returnfloat64. - (N.B.: NumPy runtime is slightly fuzzy about this, since
np.dtype(np.floating)gives float64, but with a warning because it is not a good meaning.)
- Incomplete typing means functions may return
- There is now some support for shape typing
- Previously, users could add shapes, but these were ignored.
- E.g. https://github.com/search?q=ndarray%5Btuple&type=code although 1800 files doesn't seem that much.
- Shape typing should not be used currently, because most functions will return shape-generic results, meaning that even correct shapes types will typically just type checking.
(Users could choose to use this, but probably would need to cast explicitly often.)
- Previously, users could add shapes, but these were ignored.
There is a mypy specific angle in gh-27957 to both of these, because mypy defaults (but always runs into it) to infer the type at the first assignment. This assignment is likely (e.g. creation) to include the correct shape and float64 type, but later re-assignments will fail.
mypyhas--allow-redefinitionalthough it doesn't fix it fully at least for nested scopes in for-loops,mypymay improve this.
The user impact is that:
- At least
mypyfails even for unannotated code. - Users have to avoid even correct
float64and shape types due to imprecise NumPy type stubs. These previously passed, whether intentional or not.- For
float64passing previously was arguably a bug, but is still a regression. - For shapes, this means explicitly broaden correct shapes (not necessary previously)
- For
(I, @seberg, cannot tell how problematic these are, or what options we have to try to make this easier on downstream, short of reverting or including reverting.)
Simple regressions fixed or fixable in NumPy
- TYP: False positives in
ndarray.__setitem__withobject_dtype in NumPy 2.2 #27964 - The
floatingchange has at least that seems very much fixable with follow-ups, see TYP: inconsistent static typing offloat64addition #28071 (e.g.numpy.zeros(2, dtype=numpy.float64) + numpy.float64(1.0)is clearlyfloat64). - TYP:
ndarray.itemnever typechecks #27977 - TYP:
np.ndarray.tolistreturn type seems broken in numpy 2.2.0 #27944 - TYP: excessively vague return types from
np.dtypeandnp.ndarray.dtypein numpy 2.2.0 #27945
Type-checkers issues that may impact NumPy
- MyPy has already a few new fixed related to issues found in NumPy (not sure all are 2.2 related): Issues related to numpy python/mypy#18343
Activity
jorenham commentedon Dec 31, 2024
Thanks for this analysis; it's spot on 👌🏻
Boring means predictable, and predictable means prevanteable. So by definition, regressions and bugs aren't boring 😉.
jorenham commentedon Dec 31, 2024
Maybe this is type-able if we use
@deprecatedin combination with theoptype.typing.Justtrick 🤔 .jorenham commentedon Jan 1, 2025
I believe that all cases of unannotated code that were valid with
numpy<2.2but invalid with>=2.2(such as #27957), are limited to mypy, and do not occur with(based)?pyright.There are two distinct
numpy==2.2.0changes that could cause this. One is related to the ongoing work to support shape-typing, and the other has to do with changes made tofloat64andcomplex128. Both these cases are only an issue for mypy users. But all that's needed to fix this, is to help mypy a bit by add a single type annotation.shape-typing
The shape-typing mypy issues are all caused by #27211, which changed the shape-type "default" of
ndarrayfromAnytotuple[int, ...]. Without this change, shape-typing simply wouldn't be possible to achieve practice (which has to do with the undefined behavior of@overloadin the presence ofAny).With
numpy>=2.2.0, the following (valid) code will cause mypy (and only mypy) to report an error:Since numpy 2.2.0, the return type of
np.arange(2)is anndarraywith a 1-dimensional shape-type, i.e.ndarray[tuple[int], dtype[signedinteger]].But
x + 1does not (yet) take the shape-types of the input into account, and in this case returnsNDArray[signedinteger], which resolves tondarray[tuple[int, ...], dtype[signedinteger]].It's not allowed to assign
tuple[int, ...]totuple[int], so it also isn't allowed to assignndarray[tuple[int, ...], _]to andarray[tuple[int], _].And Mypy only looks at
x = np.arange(2)when it infers the type ofx, so it immediately determines thatx: ndarray[tuple[int], dtype[signedinteger]], even if this is contradicted on the very next line.So to work around this, we need to help mypy a bit by explicitly annotating
x:float64andcomplex128Before
2.2.0,float64andcomplex128were incorrectly annotated type-aliases offloating[_64Bit]andcomplexfloating[_64Bit, _64Bit], respectively. These changes were made in #27334, and apply tocomplex128in the same way asfloat64. So for the sake of brevity, I'll limit this tofloat64.This change fixes two large issues:
float64subclassesfloating, but the stubs incorrectly annotatedfloat64as a (restricted) alias offloating. The result is thatx: float64 = floating[Any]will now be rejected, as it should (because it's type-unsafe).float64is also a subclass ofbuiltins.float, but the stubs did not reflect this. This was causing e.g.x: float = np.float64(42)to be falsely rejected onnumpy<2.2.0, even though it's perfectly type-safe.So for example, mypy (and only mypy) will reject the last of the following example
The
np.array([], np.float64)expression evaluates to aNDArray[float64]type. But adding1to this, results in aNDArray[floating]type. But sincenumpy>=2.2.0it's no longer the case thatfloatingis assignable tofloat64(because that'd be type-unsafe), so we also can't assignNDArray[floating]toNDArray[float64].This causes mypy to (falsely) reject
y = y + 1, just like it rejectedx + 1in the shape-typing example. We can also work around this in the same way:Important
Note that mypy will (still) accept the following:
We see here that
y4 + 1is inferred asndarray[tuple[int, ...], dtype[floating[Any]]], which is identical to the type ofy + 1.So the errors that mypy reports are not caused by regressions or bugs; they are the consequence of a necessary improvement. And this is why these reported mypy errors are, in fact, falsy positives.
jorenham commentedon Jan 9, 2025
Gabriel-p commentedon Jan 9, 2025
juanjosegarciaripoll commentedon Jan 26, 2025
I suppose this is related and a symptom that many methods and functions in Numpy 2.2 erase shape information, giving rise to similar problems as mentioned above
Under mypy this results in a type error for the last line
jorenham commentedon Jan 26, 2025
The lack of shape-typing support for these functions has always been the case. Since NumPy 2.2 we made several functions, including as
numpy.zeros, transparent to shape-types.Even if NumPy would have had perfect shape-typing support, your example would still be flagged as invalid, by both mypy and pyright:
The type of
outputis alist, andlistis an invariant type. With perfect shape-typing, it would only acceptfloat64arrays of shape(3,3,3). But you assign an array of shape(1,3,3). This is type-unsafe, and therefore flagged as invalid by both mypy and pyright.juanjosegarciaripoll commentedon Jan 27, 2025
This comment builds a straw man argument. First of all, I don't claim that 2.2 introduced shape erasure, but the fact that this information is absent in 2.2 and that creates conflicts with other late changes.
Second, this was a minimal example. A variable would have the same problem. But in any case lists are invariant but the inferred shape for the elements by MyPy is tuple[int, int, int] not (3,3,3). Hence the assignment is correct.
If Numpy chooses now to fixe the size of the arrays in the type that would be yet another extremely unfortunate choice because having inhomogeneous tensor sizes with a similar rank is a valid application. If the dimensions must be fixed that should be declared by the user.
jorenham commentedon Jan 27, 2025
And I didn't say that you did claim that 🤷🏻. It's just that I wanted to minimize the probability that someone else would misinterpret it that way (because exactly that has happened before, and it caused a lot of confusion).
No, it would be a different error code, and would be limited to mypy, whereas your example is also invalid on pyright.
That's indeed the type of
aat the moment, but it's far from optimal. So there's a good chance that it'll be changed to a more specific shape-type, such astuple[Literal[3], Literal[3], Literal[3]]. Once we support shape-typing inndarray.__getitem__, thena[0:1, :, :]might returntuple[int, int, int],tuple[Literal[1], Literal[3], Literal[3]], or something else entirely. My point is that the shape of an array is part of its type. So assigning an array to a list of arrays with a different is invalid.If you would've assigned
ato a variable instead, then you'd only see an error when you use mypy, and Pyright, for example, would allow it. See the shape-typing section in #28076 (comment) for why exactly that is, and how you can work around it.Hmm I don't really understand I'm afraid 🤔.
As the name suggests, the purpose of shape-typing is to statically describe the shape of arrays, not only the number of dimensions. And since the shape-type parameter and the
tupletypes are covariant, I don't see how that would make same-rank operations invalid.But at the moment, using
Literalas axis-type won't really work, because type-checkers tend to upcast e.g.Literal[42]tointwhen operated upon. There have been some ideas put forward, such as aLiteralInt, and refinement types, as a solution to this (see e.g. https://docs.google.com/presentation/d/11IKAfpS_ODE_TXmBK4BlVzx4stAcOAECOS-LF_sAzhM/edit). But both are incredibly complicated to implement, and I don't expect a solution anytime soon.So for the foreseeable future, "shape-typing" will actually mean "rank-typing" (i.e. with the tuple-of-ints types). Anything more than that will realistically require a PEP, and a rather beefy one at that.
seberg commentedon Jan 27, 2025
Any additional future type information can lead to similar things. Maybe shape-typing vs. rank-typing won't happen (even ever), but when/if it does there will be code as in the example that starts failing type-checking, because the exact shape nees to now be explicitly shape erasure.
If I understand correctly, this is a clear example where shape typing (not restricted to mypy) is inconvenient because the user must explicitly type less restrictive. I.e. where it fails Stéfan's rule of "untyped code should always pass".
We can decide that the advantages of this are larger than the disadvantages especially long-term. But the truth is that I doubt old discussions/pushes about shape typing really took these downsides into account. So we need to be very clear about them and understand how much they affect users (compared to the long-term benefits of correct shapes).
Right now, we are in the unfortunate situation that shape typing has very limited use, but does (occasionally?) require users to explicitly erase the shape.
jorenham commentedon Jan 27, 2025
If I remember correctly, Stéfan's rule only applied to valid untyped code. So it doesn't apply to the example of @juanjosegarciaripoll:
listis invariant, and therefore only accepts arrays that have the same exact shape. Updating a list with an array of a different shape is type-unsafe. But before NumPy 2.2, type-checkers wouldn't tell you that.This is what I mean with "type-unsafe":
Maybe it's not the best example, but the outcome is the same if you append a non-square matrix, e.g. of shape
(1, 2). Type-checkers are meant to prevent such mistakes, and NumPy 2.2 makes it easier for them to do so.seberg commentedon Jan 27, 2025
But I think the important thing to accept it, is that this is only type-unsafe if you assume that typing shapes is the correct/useful level of abstraction for (NumPy) array typing! And that was not the status-quo.
We can argue that clearly shapes are important, just like in your example. But that doesn't mean it right or even useful for all code.
And I think we have to accept that it may be nicer if in the example you would have to type it as
pauli : List[SquareMatrix] =to check the shapes, rather thanpaulidefaulting to it. Of course, I don't think that is possible...jorenham commentedon Jan 27, 2025
That's a very good point. And I agree that we should've thought it through better. But I'm not sure if that would've been enough, given that it was one of those "unknown unknowns".
But now that we have mypy_primer running, I'm a lot more confident that we can prevent such mistakes in the future.
But either way, even if we would've done everything right, then that
list_matmulexample would still raise aValueError; with or without shape-types. Without shape-typing it would theoretically be type-safe, but at runtime, it's just as unsafe.ignore `numpy` test shape
Jeitan commentedon Mar 19, 2025
I have a related question, I think, though if it should go somewhere else let me know - I tried to follow the above comments regarding float and np.floating but I'm a bit lost. What is the "right" way to handle float vs np.floating in user code? I'm running afoul of
np.floating[Any]not being assignable tofloat, but all I'm doing is taking the output ofnp.max()and trying to use it where a float is expected (in this case, in a matplotlib function).Like so, if you'll pardon the extremely short non-reproducible example to get the idea:
The checker in question is basedpyright running "standard" checks. I'm not quite sure what to do about such a thing without hacking up my code...
jorenham commentedon Mar 19, 2025
In the NumPy 2.2.0 release we made
np.float64an actual subclass bothnp.floatingandfloat(thebuiltinsone). Before 2.2.0, it was simply an alias ofnp.floating, which was incorrect for several reasons, which I outlined in #27334.One of the consequences of this incorrect definition on
numpy<2.2, is that type-checkers allowed you to assignx: np.floatingtoy: np.float64. But sincefloat64is a subclass offloating(at runtime), that shouldn't be allowed, because it is type-unsafe. To see why, consider this example:So you're effectively assigning a
float32tofloat64here, which is clearly type-unsafe. Before NumPy 2.2, this was just as invalid as it is now. But becausefloat64was incorrectly defined, type checkers could see that this was, in fact, an error.I understand that it can be frustrating to have to change a lot of your annotations. But it's not because of a regression that you have to do that. It's because your annotations were type-unsafe, and NumPy 2.2 made it possible for type-checkers to help you fix it.
jorenham commentedon Mar 19, 2025
The
matplotlibstubs annotate theextentparameter ofAxes.imshowashttps://github.com/matplotlib/matplotlib/blob/c887ecbc753763ff4232041cc84c9c6a44d20fd4/lib/matplotlib/axes/_axes.pyi#L480
So it doesn't accept
floating, and before NumPy 2.2.0, it also didn't acceptfloat64. If you think that it should accept something liketuple[floating * 4], then you should probably raise that with matplotlib.Jeitan commentedon Mar 19, 2025
So it seems like NumPy is (and I guess always has been) actually incompatible with the built-in float? Okay ... then is asking matplotlib to change their behavior (which no I don't think is the right thing) the only option to satisfy the type-checkers? Or using cast? I'm honestly seeking advice here since the very top of this thread is talking about user documentation.
jorenham commentedon Mar 19, 2025
Before NumPy 2.2, type-checkers rejected
x: float = np.float64(), but this is now allowed. But for the same reasons as I mentioned before,illegal: np.float64 = float()is not allowed.I at runtime matplotlib accepts both
floatandnp.floatinginput, then the type annotations should also reflect that. So asking them to fix it is indeed an option. You could also consider submitting a PR yourself.Jeitan commentedon Mar 19, 2025
Based on your explanation, I think the thing that is biting me the most is this, from the original post:
In particular, for me, it's
fftfreqreturning an array with typefloating[Any], which then propagates down through the rest of my code. I'll keep watching for updates!edited for typo
jorenham commentedon Mar 19, 2025
Yea that's understandable. I've had similar issues like that in a library I maintain that uses NumPy, so I understand how annoying it can be when you're forced to
cast(np.float64, why_isnt_this_annotated_as_f64).For what it's worth, we're putting a lot of work into improving the type signatures, e.g. by narrowing the return type in cases like yours. You can follow the progress at https://github.com/numpy/numtype, and you're welcome to help us out if feel like it, e.g. by raising issues or opening PR's for sub-optimally annotated functions like
fftfreq(for which I already opened numpy/numtype#339, btw).Jeitan commentedon Mar 19, 2025
Thanks for all the info! I had run across numtype before but not realized that it was basically the future of numpy typing. I'll check it out!
Add full type annotations to gPhoton/moviemaker/_steps.py.
Several types in numpy 2.2+ are defined as typeAliases with no underl…
jorenham commentedon Jul 16, 2025
I believe that NumPy 2.3 addresses most, if not all, of these issues. Especially if you use the new
--allow-redefinition-newmypy flag that's available since mypy 1.16, all of these issues should now be resolved.