PYTHON / NUMPY
Reshaping, stacking, and splitting
Reshape arrays without copying data, join them with concatenate or stack along the right axis, and split them back into views.
What you will learn
- Use reshape(-1, k) to let NumPy solve for one dimension from the element count
- Check np.shares_memory before writing through a reshaped or raveled array
- Pick concatenate to extend an existing axis, stack to create a new one
- Use array_split when the length does not divide evenly; split raises instead
Understanding Reshaping, stacking, and splitting
A NumPy array is a flat block of memory plus metadata: a shape and a set of strides that say how many bytes to step per axis. reshape never touches the data; it just computes new strides so the same bytes are addressed with different index arithmetic. That is why the product of the new shape must equal arr.size, and why reshaping a contiguous array costs nothing regardless of how big it is. Passing -1 for one axis tells NumPy to solve that dimension from size divided by the other dimensions.
The default traversal order is C order, meaning the last axis varies fastest, so arange(12).reshape(3, 4) fills row by row. Sometimes the requested shape cannot be expressed as strides over the existing buffer at all: reading a transposed array in C order interleaves elements that are far apart in memory. In that case reshape silently allocates and copies, so a reshaped array is only sometimes a view. Treat the result as "maybe a view" and confirm with np.shares_memory whenever you intend to write into it.
Joining and splitting are the same operation read in two directions, and both are parametrized by an axis. np.concatenate glues arrays along an axis that already exists, so every other dimension must match; np.stack inserts a brand new axis, so all inputs must have identical shapes. Concatenating and stacking always allocate a fresh buffer because the pieces live in unrelated memory, whereas np.split just hands back slices, which are views. np.split insists the axis divides evenly, while np.array_split absorbs the remainder by making the leading chunks one element longer.
import numpy as np
a = np.arange(12)
grid = a.reshape(3, 4)
print(grid)
print("view?", grid.base is a)
grid[0, 0] = 99
print("a[0] ->", a[0])
print(a.reshape(2, -1).shape)
rows = np.split(grid, 3, axis=0)
print([r.shape for r in rows])
print(np.concatenate(rows[::-1], axis=0))Shape is metadata over a flat buffer, so reshaping and splitting can be free views while concatenating and stacking must allocate and copy.
Worked examples
concatenate extends an axis, stack adds one
Shows how the same two 1-D vectors produce a length-6 vector or a 2x3 matrix depending on which function you choose.
import numpy as np
x = np.array([1, 2, 3])
y = np.array([4, 5, 6])
print(np.concatenate([x, y]).shape)
print(np.stack([x, y]).shape)
print(np.stack([x, y], axis=1))
print(np.column_stack([x, y]).shape)Example explained
Line 1concatenate joins along axis 0, the only axis a 1-D array has, so the lengths add to 6.
Line 2stack keeps both inputs intact and adds a new axis 0 of length 2, giving shape (2, 3).
Line 3stack(axis=1) puts the new axis last, so each row pairs one element from x with one from y.
Line 4column_stack is the shorthand for that pairing: it treats 1-D inputs as columns of a 2-D result.
When reshape has to copy
Demonstrates that ravel on a transposed array copies, so writes to the result do not reach the original.
import numpy as np
m = np.arange(6).reshape(2, 3)
t = m.T
print(t)
print(t.ravel())
print(np.shares_memory(t, t.ravel()))
print(np.shares_memory(m, m.ravel()))
r = t.ravel()
r[0] = 100
print(m[0, 0])Example explained
Line 1t is a view of m with swapped strides, so it is not C-contiguous.
Line 2Reading t in C order gives 0 3 1 4 2 5, an order no single stride over m's buffer can produce, so ravel allocates a copy.
Line 3np.shares_memory returns False for the transposed case and True for m.ravel(), which is a plain view.
Line 4Writing r[0] = 100 therefore changes only the copy; m[0, 0] is still 0.
Uneven splits and splitting a 2-D block
Contrasts array_split's tolerance for remainders with split's strict equal-division rule, then splits a matrix by columns.
import numpy as np
data = np.arange(10)
print([p.tolist() for p in np.array_split(data, 3)])
try:
np.split(data, 3)
except ValueError:
print("split refused: 10 is not divisible by 3")
img = np.arange(24).reshape(4, 6)
left, right = np.hsplit(img, 2)
print(left.shape, right.shape)
print(right[0])Example explained
Line 1array_split spreads the remainder over the leading chunks, so sizes are 4, 3, 3 rather than failing.
Line 2np.split raises ValueError for the same call because 10 % 3 is not zero.
Line 3hsplit(img, 2) is concatenate's inverse along axis 1, cutting the 6 columns into two blocks of 3.
Line 4right[0] starts at 3, confirming the second block holds the original columns 3 through 5.
Important notes
If you must have a view, assign to arr.shape instead of calling reshape; in-place shape assignment raises AttributeError when a copy would be needed rather than copying behind your back.
hstack on 1-D inputs joins along axis 0 and returns a longer 1-D array, not a column matrix; column_stack is what turns 1-D vectors into columns.
Common mistakes
Reshaping to a shape with the wrong element count, e.g. np.arange(10).reshape(3, 4), which raises ValueError: cannot reshape array of size 10 into shape (3,4) — the fix is checking size, not adding axes.
Assuming reshape or ravel always aliases the original: after a transpose or fancy indexing they copy, so in-place edits to the result vanish silently instead of updating the source array.
Reaching for np.stack when concatenation was meant: stacking two shape-(3,) vectors gives (2, 3) instead of (6,), and stacking arrays of differing shapes fails with "all input arrays must have the same shape".
Try it yourself
Change, predict, then run
Build np.arange(24).reshape(6, 4), split it into three (2, 4) blocks with np.split along axis 0, rejoin the blocks in reverse order with np.concatenate, and use np.shares_memory to show that each block is a view of the original while the rejoined array is not.
Open the Python workspaceCheck your understanding
a is a C-contiguous array of shape (2, 3). You compute b = a.T.reshape(-1), then set b[0] = 99, but a is unchanged. Why?
- Reading a.T in C order cannot be expressed as strides over a's buffer, so reshape returned a copy
- reshape always returns a copy, so no reshaped array ever shares memory with its source
- Assignments into a flattened array are ignored by NumPy unless you use .flat
- Passing -1 forces a copy because the missing dimension is only known at runtime
Show answer
reshape returns a view only when the new shape can be described by strides over the existing memory; the transposed array's C-order sequence interleaves distant elements, so NumPy must allocate a copy. Option 2 is tempting but wrong: a.reshape(3, 2) on the contiguous original does share memory, which np.shares_memory confirms.