Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
98264cb
[DEP] Deprecate setting the shape attribute of a numpy array
eendebakpt Jul 31, 2025
27ef5f2
add release note
eendebakpt Jul 31, 2025
b7842a8
update tests
eendebakpt Jul 31, 2025
9af1e83
more conversions
eendebakpt Jul 31, 2025
1695917
convert more tests
eendebakpt Aug 6, 2025
1d71fc5
ma
eendebakpt Aug 6, 2025
e8cbd57
fix generator and mrecords
eendebakpt Aug 6, 2025
354a04e
review comments
eendebakpt Aug 6, 2025
bea4884
remove debugging code
eendebakpt Aug 6, 2025
5c8a65e
remove examples
eendebakpt Aug 7, 2025
914462d
restore examples wiht only get of shape
eendebakpt Aug 7, 2025
7e06ce2
remove doc with deprecated shape setting
eendebakpt Aug 7, 2025
2e80bf0
update release note
eendebakpt Aug 7, 2025
d49ab1e
warn for all subclasses of ndarray
eendebakpt Aug 10, 2025
37d1b42
convert one more shape set
eendebakpt Aug 10, 2025
b888253
add deprecation test
eendebakpt Aug 10, 2025
5576aa2
Update numpy/_core/tests/test_umath.py
eendebakpt Aug 15, 2025
5de80b9
review comments
eendebakpt Aug 15, 2025
0ff8bb0
Merge branch 'deprecate_shape_v3' of github.com:eendebakpt/numpy into…
eendebakpt Aug 15, 2025
f096794
Apply suggestions from code review
eendebakpt Aug 24, 2025
1a237cf
Merge branch 'main' into deprecate_shape_v3
eendebakpt Aug 24, 2025
198e62a
Merge branch 'main' into deprecate_shape_v3
eendebakpt Aug 27, 2025
f739d54
review comments
eendebakpt Aug 28, 2025
87a45e7
Merge branch 'main' into deprecate_shape_v3
eendebakpt Sep 4, 2025
e51276d
Merge branch 'main' into deprecate_shape_v3
eendebakpt Sep 25, 2025
f45274e
review comments
eendebakpt Oct 19, 2025
96e029b
update pr number
eendebakpt Oct 22, 2025
48cdec4
Merge branch 'main' into deprecate_shape_v3
eendebakpt Dec 15, 2025
9482110
Merge branch 'main' into deprecate_shape_v3
eendebakpt Jan 2, 2026
2ed9a48
Merge branch 'deprecate_shape_v3' of github.com:eendebakpt/numpy into…
eendebakpt Jan 2, 2026
bfed477
Update numpy/_core/src/multiarray/getset.c
seberg Jan 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/release/upcoming_changes/29536.deprecation.rst
Comment thread
mhvk marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Setting the ``shape`` attribute is deprecated
---------------------------------------------
Setting the shape attribute is now deprecated since mutating
an array is unsafe if an array is shared, especially by multiple
threads. As an alternative, you can create a new view via
`np.reshape` or `np.ndarray.reshape`. For example: ``x = np.arange(15); x = np.reshape(x, (3, 5))``.
To ensure no copy is made from the data, one can use ``np.reshape(..., copy=False)``.

Directly setting the shape on an array is discouraged, but for cases where it is difficult to work
around, e.g., in ``__array_finalize__`` possible with the private method `np.ndarray._set_shape`.

16 changes: 0 additions & 16 deletions doc/source/user/basics.copies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,22 +106,6 @@ otherwise. In most cases, the strides can be modified to reshape the
array with a view. However, in some cases where the array becomes
non-contiguous (perhaps after a :meth:`.ndarray.transpose` operation),
the reshaping cannot be done by modifying strides and requires a copy.
In these cases, we can raise an error by assigning the new shape to the
shape attribute of the array. For example::

>>> import numpy as np
>>> x = np.ones((2, 3))
>>> y = x.T # makes the array non-contiguous
>>> y
array([[1., 1.],
[1., 1.],
[1., 1.]])
>>> z = y.view()
>>> z.shape = 6
Traceback (most recent call last):
...
AttributeError: Incompatible shape for in-place modification. Use
`.reshape()` to make a copy with the desired shape.

Taking the example of another operation, :func:`numpy.ravel` returns a
contiguous flattened view of the array wherever possible. On the other hand,
Expand Down
2 changes: 1 addition & 1 deletion doc/source/user/basics.indexing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ and accepts negative indices for indexing from the end of the array. ::
It is not necessary to
separate each dimension's index into its own set of square brackets. ::

>>> x.shape = (2, 5) # now x is 2-dimensional
>>> x = x.reshape((2, 5)) # now x is 2-dimensional
>>> x[1, 3]
8
>>> x[1, -1]
Expand Down
2 changes: 1 addition & 1 deletion numpy/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2208,7 +2208,7 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]):
@property
def shape(self) -> _ShapeT_co: ...
@shape.setter
@deprecated("In-place shape modification will be deprecated in NumPy 2.5.", category=PendingDeprecationWarning)
@deprecated("In-place shape modification has been deprecated in NumPy 2.5.")
def shape(self, value: _ShapeLike) -> None: ...

#
Expand Down
16 changes: 1 addition & 15 deletions numpy/_core/_add_newdocs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2885,7 +2885,7 @@

.. warning::

Setting ``arr.shape`` is discouraged and may be deprecated in the
Setting ``arr.shape`` is deprecated and may be removed in the
future. Using `ndarray.reshape` is the preferred approach.

Examples
Expand All @@ -2897,20 +2897,6 @@
>>> y = np.zeros((2, 3, 4))
>>> y.shape
(2, 3, 4)
>>> y.shape = (3, 8)
>>> y
array([[ 0., 0., 0., 0., 0., 0., 0., 0.],
[ 0., 0., 0., 0., 0., 0., 0., 0.],
[ 0., 0., 0., 0., 0., 0., 0., 0.]])
>>> y.shape = (3, 6)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: cannot reshape array of size 24 into shape (3,6)
>>> np.zeros((4,2))[::2].shape = (-1,)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: Incompatible shape for in-place modification. Use
`.reshape()` to make a copy with the desired shape.

See Also
--------
Expand Down
6 changes: 2 additions & 4 deletions numpy/_core/getlimits.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,14 @@
def _fr0(a):
"""fix rank-0 --> rank-1"""
if a.ndim == 0:
a = a.copy()
a.shape = (1,)
a = a.reshape((1,))
return a


def _fr1(a):
"""fix rank > 0 --> rank-0"""
if a.size == 1:
a = a.copy()
a.shape = ()
a = a.reshape(())
return a


Expand Down
2 changes: 1 addition & 1 deletion numpy/_core/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -740,7 +740,7 @@ def fromrecords(recList, dtype=None, shape=None, formats=None, names=None,
return _array
else:
if shape is not None and retval.shape != shape:
retval.shape = shape
retval = retval.reshape(shape)

res = retval.view(recarray)

Expand Down
29 changes: 22 additions & 7 deletions numpy/_core/src/multiarray/getset.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,13 @@ array_shape_get(PyArrayObject *self, void *NPY_UNUSED(ignored))
}


static int
array_shape_set(PyArrayObject *self, PyObject *val, void* NPY_UNUSED(ignored))
NPY_NO_EXPORT int
array_shape_set_internal(PyArrayObject *self, PyObject *val)
{
int nd;
PyArrayObject *ret;
assert(val);

if (val == NULL) {
PyErr_SetString(PyExc_AttributeError,
"Cannot delete array shape");
return -1;
}
/* Assumes C-order */
ret = (PyArrayObject *)PyArray_Reshape(self, val);
if (ret == NULL) {
Expand Down Expand Up @@ -106,6 +102,25 @@ array_shape_set(PyArrayObject *self, PyObject *val, void* NPY_UNUSED(ignored))
return 0;
}

static int
array_shape_set(PyArrayObject *self, PyObject *val, void* NPY_UNUSED(ignored))
{
if (val == NULL) {
PyErr_SetString(PyExc_AttributeError,
"Cannot delete array shape");
return -1;
}

/* Deprecated NumPy 2.5, 2026-01-05 */
if (DEPRECATE("Setting the shape on a NumPy array has been deprecated"
" in NumPy 2.5.\nAs an alternative, you can create a new"
" view using np.reshape (with copy=False if needed)."
) < 0 ) {
return -1;
}

return array_shape_set_internal(self, val);
}

static PyObject *
array_strides_get(PyArrayObject *self, void *NPY_UNUSED(ignored))
Expand Down
2 changes: 2 additions & 0 deletions numpy/_core/src/multiarray/getset.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@

extern NPY_NO_EXPORT PyGetSetDef array_getsetlist[];

NPY_NO_EXPORT int array_shape_set_internal(PyArrayObject *self, PyObject *val);

#endif /* NUMPY_CORE_SRC_MULTIARRAY_GETSET_H_ */
15 changes: 15 additions & 0 deletions numpy/_core/src/multiarray/methods.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "dtypemeta.h"
#include "item_selection.h"
#include "conversion_utils.h"
#include "getset.h"
#include "shape.h"
#include "strfuncs.h"
#include "array_assign.h"
Expand Down Expand Up @@ -2875,6 +2876,16 @@ array_class_getitem(PyObject *cls, PyObject *args)
return Py_GenericAlias(cls, args);
}

static PyObject* array__set_shape(PyObject *self, PyObject *args)
{
int r = array_shape_set_internal((PyArrayObject *)self, args);

if (r < 0) {
return NULL;
}
Py_RETURN_NONE;
}

NPY_NO_EXPORT PyMethodDef array_methods[] = {

/* for subtypes */
Expand Down Expand Up @@ -3099,6 +3110,10 @@ NPY_NO_EXPORT PyMethodDef array_methods[] = {
(PyCFunction)array_dlpack_device,
METH_NOARGS, NULL},

// For deprecation of ndarray setters
{"_set_shape",
(PyCFunction)array__set_shape,
METH_O, NULL},
// For Array API compatibility
{"__array_namespace__",
(PyCFunction)array_array_namespace,
Expand Down
3 changes: 3 additions & 0 deletions numpy/_core/tests/test_deprecations.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,9 @@ def test_deprecated_strides_set(self):
x = np.eye(2)
self.assert_deprecated(setattr, args=(x, 'strides', x.strides))

def test_deprecated_shape_set(self):
x = np.eye(2)
self.assert_deprecated(setattr, args=(x, "shape", (4, 1)))

class TestDeprecatedDTypeParenthesizedRepeatCount(_DeprecationTestCase):
message = "Passing in a parenthesized single number"
Expand Down
4 changes: 2 additions & 2 deletions numpy/_core/tests/test_item_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ def test_simple(self):
tresult = list(ta.T.copy())
for index_array in index_arrays:
if index_array.size != 0:
tresult[0].shape = (2,) + index_array.shape
tresult[1].shape = (2,) + index_array.shape
tresult[0] = tresult[0].reshape((2,) + index_array.shape)
tresult[1] = tresult[1].reshape((2,) + index_array.shape)
for mode in modes:
for index in indices:
real_index = real_indices[mode][index]
Expand Down
8 changes: 6 additions & 2 deletions numpy/_core/tests/test_multiarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,13 @@ def test_attributes(self):
assert_equal(one.shape, (10,))
assert_equal(two.shape, (4, 5))
assert_equal(three.shape, (2, 5, 6))
three.shape = (10, 3, 2)
with warnings.catch_warnings(): # gh-28901
warnings.filterwarnings('ignore', category=DeprecationWarning)
three.shape = (10, 3, 2)
assert_equal(three.shape, (10, 3, 2))
three.shape = (2, 5, 6)
with warnings.catch_warnings(): # gh-28901
warnings.filterwarnings('ignore', category=DeprecationWarning)
three.shape = (2, 5, 6)
assert_equal(one.strides, (one.itemsize,))
num = two.itemsize
assert_equal(two.strides, (5 * num, num))
Expand Down
2 changes: 1 addition & 1 deletion numpy/_core/tests/test_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ def test_recarray_conflict_fields(self):
ra.mean = [1.1, 2.2, 3.3]
assert_array_almost_equal(ra['mean'], [1.1, 2.2, 3.3])
assert_(type(ra.mean) is type(ra.var))
ra.shape = (1, 3)
ra = ra.reshape((1, 3))
assert_(ra.shape == (1, 3))
ra.shape = ['A', 'B', 'C']
assert_array_equal(ra['shape'], [['A', 'B', 'C']])
Expand Down
9 changes: 5 additions & 4 deletions numpy/_core/tests/test_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ def test_noncontiguous_fill(self):
def rs():
b.shape = (10,)

assert_raises(AttributeError, rs)
with pytest.warns(DeprecationWarning): # gh-29536
assert_raises(AttributeError, rs)

def test_bool(self):
# Ticket #60
Expand Down Expand Up @@ -652,7 +653,8 @@ def test_reshape_zero_strides(self):
def test_reshape_zero_size(self):
# GitHub Issue #2700, setting shape failed for 0-sized arrays
a = np.ones((0, 2))
a.shape = (-1, 2)
with pytest.warns(DeprecationWarning):
a.shape = (-1, 2)

def test_reshape_trailing_ones_strides(self):
# GitHub issue gh-2949, bad strides for trailing ones of new shape
Expand Down Expand Up @@ -1583,8 +1585,7 @@ class Subclass(np.ndarray):
@pytest.mark.skipif(not HAS_REFCOUNT, reason="Python lacks refcounts")
def test_take_refcount(self):
# ticket #939
a = np.arange(16, dtype=float)
a.shape = (4, 4)
a = np.arange(16, dtype=float).reshape((4, 4))
lut = np.ones((5 + 3, 4), float)
rgba = np.empty(shape=a.shape + (4,), dtype=lut.dtype)
c1 = sys.getrefcount(rgba)
Expand Down
2 changes: 1 addition & 1 deletion numpy/_core/tests/test_ufunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1765,7 +1765,7 @@ def identityless_reduce_arrs():
# Not contiguous and not aligned
a = np.empty((3 * 4 * 5 * 8 + 1,), dtype='i1')
a = a[1:].view(dtype='f8')
a.shape = (3, 4, 5)
a = a.reshape((3, 4, 5))
a = a[1:, 1:, 1:]
yield a

Expand Down
6 changes: 3 additions & 3 deletions numpy/_core/tests/test_umath.py
Original file line number Diff line number Diff line change
Expand Up @@ -4842,18 +4842,18 @@ class BadArr1(np.ndarray):
def __array_finalize__(self, obj):
# The outer call reshapes to 3 dims, try to do a bad reshape.
if self.ndim == 3:
self.shape = self.shape + (1,)
Comment thread
eendebakpt marked this conversation as resolved.
self._set_shape(self.shape + (1,))

class BadArr2(np.ndarray):
def __array_finalize__(self, obj):
if isinstance(obj, BadArr2):
# outer inserts 1-sized dims. In that case disturb them.
if self.shape[-1] == 1:
self.shape = self.shape[::-1]
self._set_shape(self.shape[::-1])

for cls in [BadArr1, BadArr2]:
arr = np.ones((2, 3)).view(cls)
with assert_raises(TypeError) as a:
with pytest.raises(TypeError):
# The first array gets reshaped (not the second one)
np.add.outer(arr, [1, 2])

Expand Down
4 changes: 2 additions & 2 deletions numpy/lib/_function_base_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -5183,8 +5183,8 @@ def meshgrid(*xi, copy=True, sparse=False, indexing='xy'):

if indexing == 'xy' and ndim > 1:
# switch first and second axis
output[0].shape = (1, -1) + s0[2:]
output[1].shape = (-1, 1) + s0[2:]
output[0] = output[0].reshape((1, -1) + s0[2:])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd tend to instead generate output separately for this case, but obviously no big deal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still think if makes more sense to do,

if indexing == 'xy' and ndim > 1:
    output = [np.asanyarray(x).reshape((1, -1) + s0[2:]
              for x in xi]
else:
    output = [np.asanyarray(x).reshape(s0[:i] + (-1,) + s0[i + 1:])
              for i, x in enumerate(xi)]

But I can also see that you want to minimize changes...

output[1] = output[1].reshape((-1, 1) + s0[2:])

if not sparse:
# Return the full N-D matrix (not only the 1-D vector)
Expand Down
3 changes: 1 addition & 2 deletions numpy/linalg/_linalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,7 @@ def tensorsolve(a, b, axes=None):
a = a.reshape(prod, prod)
b = b.ravel()
res = wrap(solve(a, b))
res.shape = oldshape
return res
return res.reshape(oldshape)


def _solve_dispatcher(a, b):
Expand Down
4 changes: 2 additions & 2 deletions numpy/linalg/tests/test_linalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,8 +1000,8 @@ def do(self, a, b, tags):
np.asarray(abs(np.dot(a, x) - b)) ** 2).sum(axis=0)
expect_resids = np.asarray(expect_resids)
if np.asarray(b).ndim == 1:
expect_resids.shape = (1,)
assert_equal(residuals.shape, expect_resids.shape)
expect_resids = expect_resids.reshape((1,))
assert_equal(residuals.shape, expect_resids.shape)
else:
expect_resids = np.array([]).view(type(x))
assert_almost_equal(residuals, expect_resids)
Expand Down
Loading
Loading