Who Bears the Cost When NumPy Changes?

I’m teaching Python to scientists and engineers at Hill Air Force Base this week. Yesterday, after class, I spent some time plane-spotting—one of the occupational benefits of teaching on an Air Force base. While aircraft were coming and going, I was thinking about something considerably less interesting: a NumPy deprecation warning that popped up during class.

I had written something utterly ordinary:

a.shape = (m, n)

And NumPy warned me that direct assignment to an array’s shape is deprecated.

DeprecationWarning: Setting the shape on a NumPy array has been deprecated in NumPy 2.5.
As an alternative, you can create a new view using np.reshape (with copy=False if needed).

Wait, what??! I don’t want a new view; I want to mutate the array I already have!

I’ve been teaching NumPy for a long time—well over 100 classes, to something like 1,500 students. I’ve seen plenty of APIs come and go; Pandas, in particular, has periodically made me grumble as useful idioms disappeared while the library matured.

But NumPy? NumPy has generally been the stable foundation underneath all of that. Assigning to .shape isn’t some obscure corner of its API. It fits beautifully into both Python’s object model and the mental model I’ve taught for years.

What’s wrong with changing an array’s shape?

Python names are like sticky notes attached to objects:

>>> x = [1, 2, 3]
>>> y = x
>>> y[1] = -9
>>> x
[1, -9, 3]
>>> y is x
True

x and y refer to the same list. Mutate the list through y, and the change is visible through x.

That’s not a bug, that’s Python’s “dynamic typing” that enables everything from for loops to import aliases.

NumPy arrays have traditionally behaved exactly as I’d expect:

>>> a = np.arange(12)
>>> b = a
>>> a.shape = (3, 4)
>>> b.shape
(3, 4)

There is one array object. Both names refer to it. I changed its shape, so naturally b.shape is now (3, 4) too.

More importantly, that’s a wonderfully simple thing to explain to a scientist learning NumPy:

Here is some numerical data. The shape describes how NumPy interprets it. If the storage permits it, you can change that shape.

There’s quite a lot of useful understanding packed into that little example.

But evidently, that’s deprecated now, and the preferred approach is this:

a = a.reshape(3, 4)

Usually that doesn’t copy the underlying data. But it does create a new Python ndarray object—a new view onto the same storage, which is not the same operation. In the code above, a ends up with a new identity.

Sometimes I really do mean: mutate this object.

So why take that away?

The answer, it would seem, is Concurrency, bane of Python developers since the early days. (I’m thinking in particular of one former co-worker; usually even-keeled, I only ever heard him swear once, and it was related to asyncio [warning: here be dragons])

More specifically, NumPy is adapting to a world of free-threaded Python and increasingly concurrent numerical computation. If multiple threads can operate on the same array, allowing one thread to change structural metadata such as shape and strides underneath another creates some nasty possibilities. And that’s a legitimate engineering problem.

But my immediate reaction was: why is that my problem?

Or, more importantly, why does it have to be my students’ problem?

The scientists and engineers I teach generally aren’t building highly concurrent numerical infrastructure. They’re using Python to solve engineering problems, analyze experimental results, process measurements, build models, and understand their work because Python is supposed to be easy to learn.

If they’ve created an array and want to mutate its shape, this seems like a perfectly reasonable contract:

You asked to mutate the array. NumPy will mutate it. If something else has a reference to the same object, it will see the mutation. Don’t do this concurrently unless you know what you’re doing.

That’s pretty much how mutable Python objects have always worked.

Haven’t we already invented tensors?

This is where I start getting cranky.

NumPy was created as a pragmatic numerical tool for scientists and engineers (read about it here). Since then we’ve developed PyTorch, TensorFlow, JAX, CuPy, and an increasingly sophisticated ecosystem of tensor and array systems.

Those systems have requirements NumPy didn’t originally have: GPUs, asynchronous execution, automatic differentiation, computation graphs, accelerators, massive parallelism, and so forth.

And that’s fine. In fact, it reminds me of something the IPython project did very well.

At some point, IPython was becoming much more than an interactive Python shell. The notebook architecture had implications well beyond Python. Rather than continuing to stretch “IPython” until it meant everything (and therefore nothing), the project created Jupyter.

IPython remained IPython, and the more general abstraction got its own identity. It kept its clean conceptual boundary.

So why can’t NumPy concede some of these requirements to the tensor world?

Let PyTorch tensors make whatever concurrency concessions PyTorch needs. Let TensorFlow tensors behave however TensorFlow needs them to behave. Let JAX arrays have the semantics necessary for JAX.

And let a NumPy array remain the wonderfully pragmatic numerical object that made Python attractive to the scientists and engineers I serve in the first place.

Of course it’s not actually that simple. NumPy itself is used concurrently, and the modern numerical Python ecosystem benefits enormously from interoperability among all of these array systems.

But notice what happens when we solve that ecosystem-level problem by changing NumPy.

Who bears the cost?

That’s the question I’ve been circling around.

Making NumPy easier to use as a foundation for a concurrent array ecosystem reduces complexity for framework and library developers.

But the complexity doesn’t simply disappear — it has to go somewhere. In fact, some of it gets transferred. To me. To my students. Early in their learning process when they’re already juggling a lot of new concepts.

Now I have to change an explanation I’ve used in many dozens of classes. My students have to learn that although many Python objects are mutable, and although an ndarray‘s numerical contents are mutable, its structural interpretation is increasingly something they should regard differently.

Instead of:

a.shape = (3, 4)

I teach:

a = a.reshape(3, 4)

A student asks, “Does that create another array?”, and the answer is yes, another ndarray object, although it no longer “owns” its own data and it may or may not copy the underlying data. That leads to a discussion on views, storage, object identity, aliasing, and maybe why changing structural metadata creates problems under concurrency.

These are all interesting topics, but none of them is what the engineer was trying to learn at that moment, and that’s the cost that bothers me. It’s not the keystrokes, it’s cognitive load.

I often introduce Python by talking about the tradeoffs between design time, run time, and debug time and how Python has traditionally prioritized design time (easy to write code, linear learning curve), and debug time (Python makes it easy to write code that is easy to debug, if you know what you’re doing). Performance can come later, with more time and effort. For decades, one of Python’s great strengths in scientific computing has been that the machinery gets out of the way quickly enough that a domain expert can concentrate on the domain.

Increasingly, Python is also infrastructure for enormous software systems—AI frameworks among them. Those systems need stronger invariants, concurrency, static analysis, interoperability, and carefully specified behavior across layers of software that no single programmer can understand completely.

Those are legitimate needs, but they’re not free, and sometimes (often) the cost is paid by the scientist sitting at a prompt who now has one more abstraction to understand before getting back to the actual problem.

And sometimes it’s paid by the instructor standing in front of that scientist, surprised by yet another deprecation warning and deciding whether today’s lesson really needs a digression into free-threaded Python just to explain why changing the shape of an array isn’t supposed to work that way anymore.

Yesterday, watching an F-5 and an F-35 land after class, an analogy came to me. The F-5 first flew in 1959, and it is still useful in 2026—not because it accumulated every mission and every capability, but because it has remained very good at a narrower job.

At Hill, that job is helping F-35 pilots train against a realistic adversary. It can serve the newer system without becoming the newer system. It can adapt to a new environment without giving up its core identity.

Of course I understand that software projects are not airplanes (although you could argue that airplanes are increasingly becoming software projects), and NumPy cannot simply ignore concurrency. But usefulness and maximal generality are not the same thing. As a project accumulates new constituencies and new requirements, the technically clean solution for the larger ecosystem can impose costs on the people for whom the tool was originally built.

Those costs are easy to dismiss individually: a deprecation warning here, a slightly different idiom there, one additional concept to explain.

But, as I frequently say in my classes, cognitive load accumulates. And for scientific software in particular, the cognitive load of the tool competes directly with the cognitive load of the science.

When we change the tools, it’s worth asking not only whether the new design is cleaner or safer.

It’s worth asking who bears the cost.

Author: tim

Founder and Principal at Diller Digital

Leave a Reply

Your email address will not be published. Required fields are marked *