# PyTorch `permute` vs `transpose`: What's the Difference (and the `reshape` Bug That Scrambles Your Images)

> Source: <https://dev.to/pytorchfromgroundup/pytorch-permute-vs-transpose-whats-the-difference-and-the-reshape-bug-that-scrambles-your-7ke>
> Published: 2026-08-02 18:23:12+00:00

You loaded an image, got a tensor shaped `(batch, height, width, channels)`

, and your convolution wants `(batch, channels, height, width)`

. Stack Overflow says `permute`

. Someone else says `transpose`

. And `reshape(2, 3, 28, 28)`

gives you the right shape too — so why is everyone making this complicated?

Because two of those three are the same tool, and the third one silently destroys your data.

`transpose(dim0, dim1)`

swaps **exactly two** dimensions. `permute(...)`

reorders **all** of them in one call, and you must list every dimension. `transpose`

is a special case of `permute`

. Both return a **view** — no data is copied, only the strides change — which also means both leave you with a non-contiguous tensor.

`reshape`

is not in this family at all. It reinterprets the flat memory under a new shape without moving anything, so it can produce the shape you asked for while completely scrambling what the numbers mean.

``` python
import torch

t = torch.arange(24).reshape(2, 3, 4)

print(t.transpose(0, 1).shape)     # torch.Size([3, 2, 4])  — swapped dims 0 and 1
print(t.permute(2, 0, 1).shape)    # torch.Size([4, 2, 3])  — full reorder
```

`transpose`

— swap two axes
`transpose(dim0, dim1)`

takes two dimension indices and swaps them. Everything else stays put.

```
t = torch.arange(24).reshape(2, 3, 4)

print(t.shape)                  # torch.Size([2, 3, 4])
print(t.transpose(0, 1).shape)  # torch.Size([3, 2, 4])
print(t.transpose(1, 2).shape)  # torch.Size([2, 4, 3])
```

The order of the two arguments doesn't matter — `t.transpose(0, 1)`

and `t.transpose(1, 0)`

are the same thing. A swap is a swap.

On a 2-D tensor this is the matrix transpose you already know, and `.T`

is the shorthand:

```
m = torch.arange(6).reshape(2, 3)
print(m.T.shape)                # torch.Size([3, 2])
print(m.transpose(0, 1).shape)  # torch.Size([3, 2])  — identical
```

**One caution on .T:** on tensors with more than two dimensions,

`.T`

reverses `.mT`

```
t = torch.arange(24).reshape(2, 3, 4)
print(t.mT.shape)   # torch.Size([2, 4, 3]) — swaps the last two dims, batch untouched
```

Reserve plain `.T`

for 2-D matrices. On anything higher, say what you mean with `permute`

or `.mT`

.

`permute`

— state the whole new order
`permute`

doesn't swap; it *rewrites the dimension order in full*. You pass the old index of each dimension in the position you want it to end up.

```
t = torch.arange(24).reshape(2, 3, 4)
print(t.permute(2, 0, 1).shape)   # torch.Size([4, 2, 3])
```

Read `permute(2, 0, 1)`

as: **"new dim 0 is old dim 2, new dim 1 is old dim 0, new dim 2 is old dim 1."** Not "move dim 2 somewhere" — you are writing out the destination order, left to right.

Two consequences worth internalising:

`RuntimeError: number of dims don't match in permute`

.`permute`

:

```
t = torch.arange(24).reshape(2, 3, 4)

via_permute    = t.permute(1, 2, 0)                    # torch.Size([3, 4, 2])
via_transposes = t.transpose(0, 1).transpose(1, 2)     # torch.Size([3, 4, 2])

print(torch.equal(via_permute, via_transposes))   # True
```

Same result. The `permute`

version says the destination shape out loud; the chained version makes the reader simulate two swaps in their head. Prefer `permute`

for anything beyond a single swap.

`permute`

is not `reshape`

Here is the bug this article exists for. You have a batch of images in `(N, H, W, C)`

— the layout you get from OpenCV, PIL, TensorFlow, and most image files — and PyTorch convolutions need `(N, C, H, W)`

.

Both of these produce a tensor of the right shape:

```
img = torch.arange(1 * 2 * 2 * 3).reshape(1, 2, 2, 3)   # N=1, H=2, W=2, C=3

print(img.permute(0, 3, 1, 2).shape)   # torch.Size([1, 3, 2, 2])  ✅
print(img.reshape(1, 3, 2, 2).shape)   # torch.Size([1, 3, 2, 2])  ⚠️ same shape!
```

Same shape. No error. No warning. Now look at what's actually inside the first channel:

```
print(img.permute(0, 3, 1, 2)[0, 0])
# tensor([[0, 3],
#         [6, 9]])            ← the red value of each of the 4 pixels ✅

print(img.reshape(1, 3, 2, 2)[0, 0])
# tensor([[0, 1],
#         [2, 3]])            ← pixel 0's R, G, B and then pixel 1's R ❌
```

The `permute`

version collected the red channel: pixels are stored as `(R,G,B)(R,G,B)…`

, so the reds are elements 0, 3, 6, 9. That's a real red channel.

The `reshape`

version just took the first four numbers in memory and called them "channel 0" — that's one whole pixel plus a third of the next one. Your "red channel" is now a mixture of red, green and blue from different pixels. The tensor is the right shape, the model trains, the loss goes down a little, and the accuracy is quietly terrible. Nothing on screen ever tells you.

The rule:`permute`

movesdimensions.`reshape`

re-cuts thesame flat sequence of numbersinto new brackets. If you want to change what an axismeans, you need`permute`

— always.

If you want the full picture on how `reshape`

re-cuts memory, that's covered in [Reshape vs View in PyTorch](https://dev.to/pytorchfromgroundup/reshape-vs-view-in-pytorch-whats-the-difference-and-when-view-breaks-4a9o).

**Images — channels-last to channels-first:**

```
img = torch.randn(2, 28, 28, 3)          # N, H, W, C  (from PIL / OpenCV)
x = img.permute(0, 3, 1, 2)              # N, C, H, W  (what nn.Conv2d wants)
print(x.shape)                           # torch.Size([2, 3, 28, 28])
```

**Attention — splitting into heads.** This is the one place where `reshape`

and `permute`

correctly appear back-to-back, and seeing why makes the distinction click:

```
B, S, E, H = 2, 5, 8, 2                  # batch, seq_len, embed_dim, heads

x = torch.randn(B, S, E)                 # torch.Size([2, 5, 8])
x = x.reshape(B, S, H, E // H)           # torch.Size([2, 5, 2, 4])  — split, don't move
x = x.permute(0, 2, 1, 3)                # torch.Size([2, 2, 5, 4])  — move heads forward
```

The `reshape`

is legitimate here because it only *splits* the last axis — the 8 embedding values were already laid out contiguously, so cutting them into 2 groups of 4 doesn't reorder anything. The `permute`

then genuinely moves the head axis in front of the sequence axis. Splitting an axis is `reshape`

work; moving an axis is `permute`

work.

`transpose`

and `permute`

never touch the underlying data. They change the tensor's **strides** — the step size PyTorch takes through memory for each dimension:

```
a = torch.arange(6).reshape(2, 3)
print(a.stride(), a.is_contiguous())            # (3, 1) True

b = a.permute(1, 0)
print(b.shape, b.stride(), b.is_contiguous())   # torch.Size([3, 2]) (1, 3) False
```

That's why they're free. It's also why the very next `.view()`

you call will blow up:

```
b.view(6)
# RuntimeError: view size is not compatible with input tensor's size and stride
# (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.
```

Two fixes, and the choice matters:

```
b.reshape(6)                # works — copies quietly when it has to
b.contiguous().view(6)      # works — reorders memory first, then views
```

Use `.reshape()`

by default. Reach for an explicit `.contiguous()`

when you're about to do many operations on the permuted tensor and want the memory laid out well once, up front, rather than copied repeatedly.

`reshape`

to convert NHWC → NCHW.`permute(0, 3, 1, 2)`

.`permute(2, 0, 1)`

as instructions to move things.`args[i]`

. Say it out loud before you trust it.`permute`

needs all of them → `RuntimeError: number of dims don't match in permute`

.`.T`

on 3-D+ tensors.`.mT`

for a batch of matrices, or `permute`

to say it exactly.`.view()`

right after a permute.`.reshape()`

or `.contiguous().view()`

.`print(x.stride())`

next to `print(x.shape)`

when a shape bug won't make sense — the strides usually tell you the real story.`transpose(a, b)`

swaps two dimensions; `permute(...)`

rewrites the whole dimension order and needs every index listed. They do the same kind of work, and `permute`

is the one to reach for whenever more than one swap is involved. Both are free view operations that leave the tensor non-contiguous, so the next `.view()`

will error — use `.reshape()`

.

And the part that costs people a weekend: ** reshape is not a reordering tool.** It can hand you a correctly-shaped tensor full of scrambled values, with nothing on screen to warn you. When an axis needs to

**More in this series:** [Reshape vs View](https://dev.to/pytorchfromgroundup/reshape-vs-view-in-pytorch-whats-the-difference-and-when-view-breaks-4a9o) · [Broadcasting Explained](https://dev.to/pytorchfromgroundup/pytorch-broadcasting-explained-the-3-rules-and-the-silent-bug-that-bites-everyone-3606) · [What unsqueeze Does](https://dev.to/pytorchfromgroundup/what-does-unsqueeze-do-in-pytorch-and-why-your-model-keeps-asking-for-it-181a) ·

`keepdim`

Does**A question for you:** what's the shape bug that cost you the most time? I'm collecting the ones that hit hardest — drop it in the comments and I'll write up the fix.

This is one idea from my book *PyTorch From Ground Up*, which builds everything from tensors upward so nothing stays vague. If it helped, you can grab the [free PyTorch tensor cheat-sheet here](https://payhip.com/b/7ukxh), run every example from the book in the [companion notebooks on GitHub](https://github.com/pytorch-from-ground-up/book_code), get the [digital edition on Leanpub](https://leanpub.com/pytorchfromgroundup), or find the [full paperback on Amazon here](https://www.amazon.com/dp/B0H8WMCV33).
