Reordering axes without moving a single byte - and the one case where .T silently does nothing.
Overview
Strides again
Transposing an array swaps its axes. For a 2-D array, rows become columns and columns become rows. For higher-dimensional arrays, the axes are permuted in a specified order. The operation is instant because no data is moved. NumPy simply changes the strides and the shape in the array description.
That zero-cost property is worth understanding. A 2-D array with shape (1000, 2000) uses the same memory as its transpose. np.shares_memory confirms that the transpose is a view of the original. Writing to the transpose changes the original, because they are the same block of bytes with different descriptions.
The strides tell NumPy how many bytes to step for each axis. A C-contiguous (1000, 2000) float64 array has strides (16000, 8). Each row is 16000 bytes long. Each element within a row is 8 bytes. The transpose swaps the strides to (8, 16000). Now stepping along the first axis moves 8 bytes, which walks through what used to be columns.
For 2-D arrays, the shorthand a.T is the standard notation. For higher dimensions, np.transpose with an axes argument gives you full control. np.swapaxes swaps exactly two axes, which is simpler when that is all you need.
The practical consequence of strides is memory layout. A C-contiguous array has its last axis changing fastest. A Fortran-contiguous array has its first axis changing fastest. A transposed C-contiguous array is Fortran-contiguous. Operations that scan along the wrong axis are slower because they jump through memory instead of walking sequentially.
That performance difference is small for most work. It matters for tight numerical loops and for interoperating with libraries that expect a specific memory order. np.ascontiguousarray copies the data into C order when you need it.
This module covers transpose for 2-D and higher-dimensional arrays, the strides that make it free, the memory-order conventions, and swapaxes and moveaxis for targeted axis manipulation. The examples verify the view property and show how strides change.
An array knows how many bytes to step for each axis. Transposing swaps those numbers along with the shape. The underlying buffer is untouched.
That is why a.T on a gigabyte array returns instantly. It's a view, and np.shares_memory(a, a.T) confirms it.
all else in this module follows from that one fact.
Worth knowing
.T reverses the axes by swapping strides. No data moves, so transposing a huge array is free.
.T on a 1-D array does nothing. Use v[:, None] to get a column.
On three or more axes .T reverses all of them. Use transpose(...) with an explicit order instead.
swapaxes exchanges two axes and moveaxis relocates one — both read better than counting positions.
A transposed array is no longer C-contiguous, so ravel and some reshape calls will quietly copy.
Transpose reflects along the diagonal; it is not a rotation. rot90, flipud and fliplr are the geometric operations.
Transpose and Moving Axes
Reordering axes without moving a single byte.
.T reverses the axes
It is a view: the strides swap and the data never moves.
example_01.pyNumPy
import numpy as np
a = np.arange(6).reshape(2, 3)
print("a", a.shape); print(a)
print()
print("a.T", a.T.shape); print(a.T)
print()
print("strides a :", a.strides)
print("strides a.T :", a.T.strides, "<- the same numbers, swapped")
print("shares memory:", np.shares_memory(a, a.T))
print()
print("Transposing a 1 GB array costs nothing. No byte is copied.")
Output
The 1-D trap
.T on a 1-D array is a no-op. This surprises everyone once.
example_02.pyNumPy
import numpy as np
v = np.array([1, 2, 3])
print("v :", v.shape)
print("v.T :", v.T.shape, "<- unchanged. There is no second axis to swap.")
print()
print("To get a column, add an axis:")
print(" v[:, None] ->", v[:, None].shape)
print(" v.reshape(-1, 1) ->", v.reshape(-1, 1).shape)
print(v[:, None])
print()
print("This is why a 'row minus column' outer operation needs None,")
print("not .T:")
print(v[:, None] - v)
Output
v.T where v has shape (3,) gives shape (3,). Nothing happens.
There's no second axis to swap. The step is meaningless and NumPy performs it silently rather than raising. People coming from MATLAB or from linear algebra form expect a column vector and get their input back.
The fix is to add an axis: v[:, None] gives (3, 1). v[None, :] gives (1, 3). reshape(-1, 1) does the same job.
This is also why an outer-style step is written v[:, None] - v rather than v.T - v. The first broadcasts (3,1) against (3,) to give (3,3). The second subtracts an array from itself and gives zeros.
transpose with an explicit order
On more than two axes, .T reverses all of them - which is rarely what you want. Name the order instead.
example_03.pyNumPy
import numpy as np
a = np.arange(24).reshape(2, 3, 4)
print("a :", a.shape)
print("a.T :", a.T.shape, "<- fully reversed")
print()
print("transpose names the NEW order in terms of the old positions:")
print(" transpose(1, 0, 2) ->", a.transpose(1, 0, 2).shape)
print(" transpose(0, 2, 1) ->", a.transpose(0, 2, 1).shape)
print(" transpose(2, 0, 1) ->", a.transpose(2, 0, 1).shape)
print()
print("Read (2,0,1) as: old axis 2 first, then old axis 0, then old axis 1.")
Output
swapaxes and moveaxis read better
Two axes to exchange, or one axis to relocate - both clearer than counting positions.
example_04.pyNumPy
import numpy as np
a = np.arange(24).reshape(2, 3, 4)
print("swapaxes(0, 2) ->", np.swapaxes(a, 0, 2).shape, "exchange two axes")
print("moveaxis(a, 0, -1) ->", np.moveaxis(a, 0, -1).shape, "send axis 0 to the end")
print("moveaxis(a, -1, 0) ->", np.moveaxis(a, -1, 0).shape, "bring the last to front")
print()
print("The classic use: images stored (channels, h, w) vs (h, w, channels)")
img = np.zeros((3, 64, 48))
print(" CHW", img.shape, "-> HWC", np.moveaxis(img, 0, -1).shape)
print()
print("all of these are views:", np.shares_memory(a, np.moveaxis(a, 0, -1)))
Output
Transposing makes the memory non-contiguous
Which is fine, until something needs a contiguous buffer and quietly copies.
example_05.pyNumPy
import numpy as np
a = np.arange(12).reshape(3, 4)
t = a.T
print("a is C-contiguous:", a.flags["C_CONTIGUOUS"])
print("t is C-contiguous:", t.flags["C_CONTIGUOUS"],
" F-contiguous:", t.flags["F_CONTIGUOUS"])
print()
print("ravel on a view that is not contiguous must copy:")
print(" shares memory with a:", np.shares_memory(a, t.ravel()))
print()
print("reshape can fail to be a view for the same reason:")
r = t.reshape(-1)
print(" t.reshape(-1) shares memory:", np.shares_memory(a, r))
print()
print("ascontiguousarray makes the copy explicit when you want it:")
c = np.ascontiguousarray(t)
print(" c is C-contiguous:", c.flags["C_CONTIGUOUS"])
Output
Transpose is not a rotation
It reflects along the diagonal. If you wanted to turn an image, rot90 is the function.
example_06.pyNumPy
import numpy as np
a = np.array([[1, 2],
[3, 4]])
print("original:"); print(a)
print()
print("a.T (reflected along the diagonal):"); print(a.T)
print()
print("np.rot90(a) (turned a quarter turn anticlockwise):"); print(np.rot90(a))
print()
print("np.flipud / np.fliplr mirror instead:")
print(" flipud:", np.flipud(a).tolist())
print(" fliplr:", np.fliplr(a).tolist())
print()
print("rot90 twice == flipping both ways:",
np.array_equal(np.rot90(a, 2), np.flipud(np.fliplr(a))))
Output
More than two axes
.T reverses all axes. On shape (2, 3, 4) it gives (4, 3, 2).
That is well defined but rarely what anyone wants. With three or more axes the notable step is often moving one exact axis one spot.
a.transpose(2, 0, 1) names the new order by old spots. Old axis 2 first, then old axis 0, then old axis 1. Reading it in that direction. "where does each new axis come from". Is the way to keep it straight.
swapaxes and moveaxis
Both are clearer than counting.
np.swapaxes(a, 0, 2) exchanges two axes and leaves the rest alone.
np.moveaxis(a, 0, -1) takes axis 0 and puts it at the end, sliding the others along. This is the canonical fix for image layout. Deep learning frameworks want (channels, height, width), image libraries want (height, width, channels). moveaxis converts between them for free.
Both return views.
Contiguity, and the copy you did not ask for
An array created by reshape on fresh data is C-contiguous. The last axis varies fastest and the elements sit in memory in the order you'd read them.
Transposing breaks that. The transposed view is F-contiguous instead. The same buffer, read column-first.
Most steps don't care. But anything that needs a flat contiguous buffer must copy:
ravel() returns a view when it can and a copy when it can't. On a transposed array, it can't.
reshape has the same action. This Is why "reshape returns a view" carries a *often*.
Neither warns you. If it matters. Because the array is large, or because you were leaning on writing through the result. Check with np.shares_memory, or force the issue with np.ascontiguousarray so the copy is clear and happens where you can see it.
Transpose is not rotation
This trips people up on image data.
Transpose reflects along the main main-line. Rotating a quarter turn is np.rot90. Mirroring is np.flipud (up-down) and np.fliplr (left-right).
They're related — a rotation is a transpose followed by a flip — but they're other steps. Reaching for .T to turn an image gives a mirrored result that looks almost right and isn't.
C order and F order, stated plainly
An array's elements are stored in one flat run of memory. The order decides which axis varies fastest as you walk that run.
C order — NumPy's default, named for the C language — means the last axis varies fastest. Row by row.
Fortran order means the first axis varies fastest. Column by column.
A transposed C-sorted array is F-contiguous. Reading the transpose row by row is reading the first column by column. Nothing moved. The same bytes are simply given differently.
Both flags can be true at once for a 1-D array or an array with a length-1 axis. Both can be false for a strided slice like a[::2]. a.flags reports all of it.
Where this turns hands-on: some LAPACK routines want Fortran order and will copy inside if they don't get it. Interfacing with Fortran or MATLAB-derived code at times needs np.asfortranarray. For all else, staying in C order and not thinking about it's right.
expand_dims and squeeze at boundaries
The length-1 axis is the currency of shape negotiation between libraries.
A model trained on batches expects (batch, features) and gets a single sample of shape (features,). x[None] or np.expand_dims(x, 0) adds the batch axis.
A result comes back as (n, 1) and the next function wants (n,). squeeze drops it.
expand_dims(a, axis) is the clear form of a[None]. Reads better when the axis spot is found rather than literal.
squeeze with no arg drops each length-1 axis. This Is the source of a truly nasty intermittent bug. A batch of one sample loses its batch dim. The code works for each batch size except one. Always name the axis. squeeze(axis=1). In code that handles var batch sizes.
einsum as general axis manipulation
Once an step needs a transpose, two newaxis insertions and a sum over a given axis, np.einsum often expresses it more clearly.
The subscript string names an index for each axis of each operand and says which live:
"ij->ji" is a transpose. "ii->i" extracts a main-line. "ij->j" sums over rows. "ij,jk->ik" is mat product. "bij,bjk->bik" is batched mat product.
Indices that appear on the left but not the right are summed over. Indices repeated between operands are matched and times'd.
It isn't always fast — for plain mat products, @ dispatches to a tuned BLAS routine and einsum may not. Its edge is legibility. The string states the step. Here A chain of transposes and broadcasts states only the mechanics.
np.einsum_path can be used to inspect and optimise the contraction order for expressions with a few operands. Here The order truly matters.
The batch-axis convention
Most numeric code follows one rule: the batch axis comes first. The axes being operated on come last.
(batch, height, width, channels) for images in TensorFlow. (batch, channels, height, width) in PyTorch. (samples, features) for tabular data.
The reason the operated-on axes go last is broadcasting. Trailing axes align on its own. A per-channel or per-feature param of shape (channels,) mixes with the data without any index juggling. Put the channel axis first and the same step needs [:, None, None].
matmul follows the same rule: it treats the last two axes as the mat and broadcasts all before them. A (batch, n, k) @ (k, m) works right.
Following the rule in your own arrays means broadcasting and matmul cooperate with you rather than needing a transpose at each boundary.
In practice
Use .T freely on 2-D and never on 1-D.
Use moveaxis or swapaxes on higher dims. They name what you meant and .T reverses all.
note that all of them return views over the same bytes. A later ravel or reshape may copy without telling you. Check with np.shares_memory when it matters, or force it with np.ascontiguousarray.
And keep the batch axis first. Most of the shape gymnastics people write is the cost of having put it one spot else.
Diagonals and axis-aware helpers
np.main-line(a) extracts the main main-line and returns a view in modern NumPy. Read-only. Writing to it has no steady meaning across the strides involved.
np.trace(a) sums the main-line without materialising it.
np.fill_diagonal(a, value) writes to the main-line in place. This Is the supported way to modify it. It's the standard step when building an adjacency or distance mat that should have zeros on the main-line.
offset on main-line selects the super- or sub-diags. This Is how you extract a band.
rollaxis, and why not to use it
Older code uses np.rollaxis. This Moves an axis but with arg semantics that are truly hard to reason about. The destination is read differently based on direction.
np.moveaxis was added namely to replace it, with the obvious semantics: source and destination, both plain spots.
If you meet rollaxis in existing code, translating it to moveaxis and checking the resulting shape is often worth doing while you're there. If you're writing new code, there's no reason to use it.
Checking that a transform did what you meant
Axis handling is the easiest place in NumPy to give a result that has the right shape and the wrong contents.
Two checks catch nearly all of it.
Verify a known element. After moveaxis(a, 0, -1), confirm that b[i, j, k] == a[k, i, j]. One assertion docs the transform better than a comment does.
Use distinguishable test data.np.arange(24).reshape(2, 3, 4) has a unique value in each spot. Any misplacement is visible. An array of ones or of random values hides just the errors you're looking for.
This is worth doing because the failure mode. Correct shape, permuted contents. Makes no error and often no obviously wrong output, just results that are subtly worse than they should be.
The summary
.T — reverses all axes. Right for 2-D, wrong for most higher-dimensional intent. Silently a no-op on 1-D.
transpose(order) — clear permutation, read as "where each new axis comes from".
swapaxes(i, j) — exchange two, leave the rest.
moveaxis(src, dst) — relocate one, slide the rest. The clearest of the four. The right default for anything above 2-D.
expand_dims / squeeze — add and drop length-1 axes, with squeeze needing an clear axis in any code that handles var batch sizes.
einsum — when the index bookkeeping has become the substance of the step.
All of them return views. That is what makes them free. It's also why a later ravel or reshape may copy without saying so. np.shares_memory when it matters, np.ascontiguousarray when you want the copy to happen one spot visible.
A closing note on cost
each step in this module is free. That is worth stating once more because it changes how you write code.
There's no reason to avoid a transpose, a moveaxis or an expand_dims on speed grounds. They adjust a handful of ints showing the array and return at once, what its size.
The cost, when it comes, arrives later. At the point a thing needs contiguous memory and quietly copies. That is where to look when a pipeline is slower than the steps in it suggest. a.flags plus np.shares_memory will tell you within a minute.
Questions people ask about Transpose and Moving Axes
Does transpose copy the data? No. It returns a view with the strides swapped, so it is instant. Some later operations may then need to copy.
What does a.T do for a 1D array? Nothing, because there is only one axis. Turn the vector into a column with a[:, None] if you need a 2D shape.
How do I transpose a 3D array? Use a.transpose(order), listing the new axis order, for example a.transpose(2, 0, 1). a.T just reverses all the axes.
What do swapaxes and moveaxis do? swapaxes exchanges two axes. moveaxis moves one axis to a new position and leaves the others in order, which is often clearer.
Why is my transposed array slower to process? It is no longer C-contiguous, so memory access jumps around. np.ascontiguousarray makes a copy in the order you want.
How do I turn images from HWC to CHW? Use np.moveaxis(img, -1, 0) or img.transpose(2, 0, 1). Both are views until you force a copy.
Where axis work fits
Reordering axes is almost always a translation step: between how data arrived and how the next function wants it.
That means the right question is rarely "how do I transpose this" but "what layout does the thing I am calling expect". Answering that first often reveals that one moveaxis at the boundary replaces a few scattered ones inside. That keeping a single rule throughout drops most of them fully.
Check yourself
0 of 4
Answer without scrolling back up.
What does `.T` do to a 1-D array of shape (3,)?
There is no second axis to swap, and NumPy does it silently rather than raising. Use `v[:, None]` for a column.
Why is transposing a 1 GB array instant?
The strides simply swap. `np.shares_memory(a, a.T)` is True - it is a view over the same bytes.
You have an image array shaped (3, 64, 48) and need (64, 48, 3). What is the cleanest call?
`.T` would give (48,64,3) by reversing everything, and reshape would scramble the data. moveaxis relocates one axis and slides the rest.
Why can `ravel()` on a transposed array return a copy?
The same reason `reshape` returns a view only *usually*. Check with np.shares_memory, or use np.ascontiguousarray to make the copy explicit.
Cheat sheet
Transpose and Moving Axes
Transpose a NumPy array and reorder its axes with swapaxes and moveaxis. See how the operation changes strides without copying any data.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.