You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Not necessarily related to numtype but NumPy typing in general. I'm a bit confused as to what is expected to work in user code and what isn't. Is there some documentation which explains this? Perhaps a page or simple list of features would help elucidate this for users.
As a concrete example - I'd like a type checker to be able to pick up this example related to reducing rank of the array. This should fail, but currently passes with mypy v1.19.1 NumPy v2.4.0 Python 3.14. Is this sort of thing even possible without language changes or runtime typing? Is support for this on the horizon?
fromtypingimportNewTypefromnumpyimportndarrayimportnumpyasnpA=NewType("A", int)
B=NewType("B", int)
C=NewType("C", int)
type U8=np.dtype[np.uint8]
# Should fail - currently pass. Actual returned type is `ndarray[tuple[B], U8]`deff1(a: ndarray[tuple[A, B, C], U8]) ->ndarray[tuple[A, B], U8]:
returna[:, 2, :]
rng=np.random.default_rng(0)
a: ndarray[tuple[A, B, C], U8] =rng.integers(0, 100, size=(10, 20, 30), dtype=np.uint8)
print(f1(a).shape)
I think this is a similar question as #578, perhaps the same one.
The TL;DR is: numtype only supports shape-types that are tuples of just ints, so no Literal ints, and also no int subtypes such as NewType. We also cannot support this in the future because of the limitations of Python's type system, specifically due to the lack of "const type parameters", see #578 (comment)
Thanks for the response, I hadn't seen that issue. I'm not sure I fully grokked that conversation. If I understood correctly, the argument is that shape typing is not guaranteed to be tracked reliably through all operations, and so it's pointless (and gives a false sense of security)?
I do note that the example (below) currently reveals Literal[1] on ty, pyrefly & mypy, and that pyright is the only outlier upcasting to int. Does that change things? (apologies if some key points mentioned already have gone over my head)
If I understood correctly, the argument is that shape typing is not guaranteed to be tracked reliably through all operations, and so it's pointless (and gives a false sense of security)?
Yea that about sums it up 👌
I do note that the example (below) currently reveals Literal[1] on ty, pyrefly & mypy, and that pyright is the only outlier upcasting to int. Does that change things? (apologies if some key points mentioned already have gone over my head)
These differences between type-checkers are because the typing spec does not specify what should happen in these cases.
But even at some point all type-checkers would pass along all literals, then we still wouldn't be able to make use of literals in shape-typing. Because if you have some array a with shape tuple[int, int] (i.e. statically unknown axis sizes, but know ndim), then you wouldn't be able to use it in the following function:
deflinregress(X: Array[np.float64, tuple[int, Literal[2]]) ->tuple[float, float]:
"""Linear regression on an input array of shape (n, 2)."""
So because int is not assignable to Literal[2], linregress(a) (where a has shape tuple[int, int]) would be rejected by all (sane) type-checkers. So instead of tuple[int, int] we'd need some "gradual" int that is assignable to Literal[2] and vice-versa, similar to Any but limited to the domain of int.
In practice there are many cases where you simply cannot determine the exact axis size as literal. For example think of a.reshape((42, -1)), or np.concatenate for which we'd need to "add" two literal ints, which we can't do a the moment.
So because int is not assignable to Literal[2], linregress(a) (where a has shape tuple[int, int]) would be rejected by all (sane) type-checkers.
In this case it seems desirable to reject linregress(a)? I would expect the user would have to assert that a is shape tuple[int, Literal[2]] because linregress declares it only accepts that (narrow) input
So because int is not assignable to Literal[2], linregress(a) (where a has shape tuple[int, int]) would be rejected by all (sane) type-checkers.
In this case it seems desirable to reject linregress(a)? I would expect the user would have to assert that a is shape tuple[int, Literal[2]] because linregress declares it only accepts that (narrow) input
Theoretically that would be the only option that's type-safe. But in practice it'll force users to manually narrow the shape-type using assert_type or something in most situations. In my experience that'll probably cause many users to complain and open issues about this. Similar things have happened before, where I changed something in the numpy stubs to make it more type-safe and theoretically sound, but that lead to very annoying inevitable errors to be reported in valid use-cases. So making things more type-safe can in practice cause more problems than it solves. In the worst case, people get so frustrated by those falsely reported errors, that they ditch static typing altogether (which I've also seen happening), i.e. the opposite of what we aim to achieve.
Thanks for explaining, I can understand that perspective. I do think it could be useful to track shape typing through a limited set of operations & degrade gracefully where types can't be inferred (require explicit annotations) -- but I understand the practical tension here & lack of foundation from the typing spec.
Hopefully it will eventually become pragmatic to support full shape typing -- perhaps by partnering with a type checker? It does seem like ty is able to do some arithmetic on types, eg
Activity
jack-mcivor commentedon Jan 20, 2026
FWIW I was curious and had a look with mypy v1.19.1, NumPy v2.4.1, numtype v2.4.0.0a1, Python 3.14
f1as defined above still passes (should fail)f2as defined above now passes (should fail)jorenham commentedon Jan 20, 2026
Oh sorry I forgot about this.
I think this is a similar question as #578, perhaps the same one.
The TL;DR is: numtype only supports shape-types that are tuples of just
ints, so noLiteralints, and also no int subtypes such asNewType. We also cannot support this in the future because of the limitations of Python's type system, specifically due to the lack of "const type parameters", see #578 (comment)Does this answer your question?
jack-mcivor commentedon Jan 20, 2026
Thanks for the response, I hadn't seen that issue. I'm not sure I fully grokked that conversation. If I understood correctly, the argument is that shape typing is not guaranteed to be tracked reliably through all operations, and so it's pointless (and gives a false sense of security)?
I do note that the example (below) currently reveals
Literal[1]on ty, pyrefly & mypy, and that pyright is the only outlier upcasting toint. Does that change things? (apologies if some key points mentioned already have gone over my head)jorenham commentedon Jan 20, 2026
Yea that about sums it up 👌
These differences between type-checkers are because the typing spec does not specify what should happen in these cases.
But even at some point all type-checkers would pass along all literals, then we still wouldn't be able to make use of literals in shape-typing. Because if you have some array
awith shapetuple[int, int](i.e. statically unknown axis sizes, but know ndim), then you wouldn't be able to use it in the following function:So because
intis not assignable toLiteral[2],linregress(a)(whereahas shapetuple[int, int]) would be rejected by all (sane) type-checkers. So instead oftuple[int, int]we'd need some "gradual"intthat is assignable toLiteral[2]and vice-versa, similar toAnybut limited to the domain ofint.In practice there are many cases where you simply cannot determine the exact axis size as literal. For example think of
a.reshape((42, -1)), ornp.concatenatefor which we'd need to "add" two literal ints, which we can't do a the moment.jorenham commentedon Jan 20, 2026
But I agree that the documentation is lacking in this aspect, so I'll leave this open. Thanks for the write-up :)
jack-mcivor commentedon Jan 20, 2026
In this case it seems desirable to reject
linregress(a)? I would expect the user would have to assert that a is shapetuple[int, Literal[2]]because linregress declares it only accepts that (narrow) inputjorenham commentedon Jan 20, 2026
Theoretically that would be the only option that's type-safe. But in practice it'll force users to manually narrow the shape-type using
assert_typeor something in most situations. In my experience that'll probably cause many users to complain and open issues about this. Similar things have happened before, where I changed something in the numpy stubs to make it more type-safe and theoretically sound, but that lead to very annoying inevitable errors to be reported in valid use-cases. So making things more type-safe can in practice cause more problems than it solves. In the worst case, people get so frustrated by those falsely reported errors, that they ditch static typing altogether (which I've also seen happening), i.e. the opposite of what we aim to achieve.jack-mcivor commentedon Jan 20, 2026
Thanks for explaining, I can understand that perspective. I do think it could be useful to track shape typing through a limited set of operations & degrade gracefully where types can't be inferred (require explicit annotations) -- but I understand the practical tension here & lack of foundation from the typing spec.
Hopefully it will eventually become pragmatic to support full shape typing -- perhaps by partnering with a type checker? It does seem like
tyis able to do some arithmetic on types, egReally appreciate your work - rank typing is going to be really useful for type safety in a bunch of projects I work with 🎉