NumPy shape is an attribute, not a function. It hands you a tuple with one entry per dimension, and you read it with no parentheses at all:
arr.shape # (2, 3) <- correct
arr.shape() # TypeError: 'tuple' object is not callable
That single detail explains most of the errors people hit with it. The rest of this guide covers what the tuple means, what shape[0] and shape[1] actually count, and how shape differs from size, ndim and len().
The output in every block below comes from a run on Python 3.12.5, NumPy 2.5.3.
![numpy shape tuple dimensions diagram Diagram of NumPy shape tuples for 1D, 2D and 3D arrays, with shape[0] labelled as rows and shape[1] as columns](https://pythonguides.com/wp-content/uploads/2026/09/numpy-shape-tuple-dimensions-diagram.png)
What .shape returns in NumPy
Ask an array for its shape and you get a tuple back:
import numpy as np
scores = np.array([[90, 85, 72],
[65, 78, 88]])
print("the array:")
print(scores)
print()
print("scores.shape ->", scores.shape)
print("type of it ->", type(scores.shape))
print("no parentheses: it is an attribute, not a method call")
Output:
the array:
[[90 85 72]
[65 78 88]]
scores.shape -> (2, 3)
type of it -> <class 'tuple'>
no parentheses: it is an attribute, not a method call

scores.shape is a tuple, which is why it needs no parentheses.Two rows, three columns, so (2, 3). Because it is a plain tuple you can index it, unpack it, or compare it directly to another tuple with ==.
It is an attribute NumPy keeps updated, not a calculation. Reading it costs nothing, so there is no reason to cache it in a variable for speed.
How to read a shape tuple
The tuple runs from the outermost dimension to the innermost:
import numpy as np
one_d = np.array([10, 20, 30])
two_d = np.array([[1, 2, 3], [4, 3, 6]])
three_d = np.arange(24).reshape(2, 3, 4)
zero_d = np.array(7)
for name, arr in [("1D [10 20 30]", one_d),
("2D [[1,2,3],[4,3,6]]", two_d),
("3D arange(24)", three_d),
("0D just 7", zero_d)]:
print(f"{name:<24} shape {str(arr.shape):<12} ndim {arr.ndim}")
print()
print("read the tuple outside-in: the 3D array holds 2 blocks,")
print("each block has 3 rows, and each row has 4 numbers.")
Output:
1D [10 20 30] shape (3,) ndim 1
2D [[1,2,3],[4,3,6]] shape (2, 3) ndim 2
3D arange(24) shape (2, 3, 4) ndim 3
0D just 7 shape () ndim 0
read the tuple outside-in: the 3D array holds 2 blocks,
each block has 3 rows, and each row has 4 numbers.
A one dimensional array gives (3,). That trailing comma looks like a typo but it is Python’s way of writing a one-element tuple, and it is how you tell (3,) apart from the plain number 3.
A zero dimensional array, holding a single value, gives an empty tuple (). You see those when an aggregation collapses everything, as np.sum does without an axis argument.
For the 3D case, the first number counts blocks rather than rows. That is worth keeping straight when you move on to 3D arrays in NumPy.
What shape[0] and shape[1] mean
This is the part that sends people to search engines. For a 2D array, shape[0] is the number of rows and shape[1] is the number of columns:
import numpy as np
data = np.array([[5.1, 3.5, 1.4],
[4.9, 3.0, 1.4],
[6.2, 3.4, 5.4],
[5.9, 3.0, 5.1]])
print("data.shape ->", data.shape)
print("data.shape[0] ->", data.shape[0], " rows (how many samples)")
print("data.shape[1] ->", data.shape[1], " columns (how many features)")
print()
rows, cols = data.shape
print(f"unpacked in one line: rows={rows}, cols={cols}")
print()
print("the loop you see everywhere:")
for i in range(data.shape[0]):
print(f" row {i} -> {data[i]}")
print()
try:
data.shape[2]
except IndexError as err:
print("data.shape[2] -> IndexError:", err)
print("a 2D array only has shape[0] and shape[1]")
Output:
data.shape -> (4, 3)
data.shape[0] -> 4 rows (how many samples)
data.shape[1] -> 3 columns (how many features)
unpacked in one line: rows=4, cols=3
the loop you see everywhere:
row 0 -> [5.1 3.5 1.4]
row 1 -> [4.9 3. 1.4]
row 2 -> [6.2 3.4 5.4]
row 3 -> [5.9 3. 5.1]
data.shape[2] -> IndexError: tuple index out of range
a 2D array only has shape[0] and shape[1]
![numpy shape 0 rows shape 1 columns command prompt Command Prompt output showing shape[0] as the row count and shape[1] as the column count of a NumPy array](https://pythonguides.com/wp-content/uploads/2026/09/numpy-shape-0-rows-shape-1-columns-command-prompt.png)
shape[0] counts rows, shape[1] counts columns.In machine learning code that reads as shape[0] samples and shape[1] features, which is why for i in range(data.shape[0]) appears in so many examples. It simply loops once per row.
Unpacking is usually clearer: rows, cols = data.shape names both numbers in one line and fails loudly if the array is not 2D.
| Expression | On a 2D array | On a 3D array |
|---|---|---|
shape[0] | rows | blocks |
shape[1] | columns | rows |
shape[2] | IndexError | columns |
shape[-1] | columns | columns |
shape[-1] is the habit worth picking up. It always means the innermost dimension, so it keeps working when an extra dimension appears in front of your data.
Why .shape() raises TypeError
Adding parentheses is the single most common mistake here:
import numpy as np
arr = np.array([[1, 2], [3, 4]])
print("correct arr.shape ->", arr.shape)
try:
arr.shape()
except TypeError as err:
print("wrong arr.shape() -> TypeError:", err)
print()
print("there IS a function called np.shape, and it does take parentheses:")
print("np.shape(arr) ->", np.shape(arr))
print("np.shape([[1, 2, 3]]) ->", np.shape([[1, 2, 3]]))
Output:
correct arr.shape -> (2, 2)
wrong arr.shape() -> TypeError: 'tuple' object is not callable
there IS a function called np.shape, and it does take parentheses:
np.shape(arr) -> (2, 2)
np.shape([[1, 2, 3]]) -> (1, 3)

arr.shape() tries to call a tuple, which raises TypeError.The message is accurate. arr.shape has already given you the tuple (2, 2), and the extra parentheses then try to call that tuple like a function.
There is a separate np.shape function, and that one does take parentheses. It works on anything array-like, including lists, which makes it the safer choice in code that accepts either.
shape vs size vs ndim vs len
Four ways to measure an array, and they answer four different questions:
import numpy as np
grid = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 0, 1, 2]])
print("grid is 3 rows by 4 columns")
print()
print("grid.shape ->", grid.shape, " the full picture, as a tuple")
print("grid.size ->", grid.size, " total elements (3 * 4)")
print("grid.ndim ->", grid.ndim, " how many dimensions")
print("len(grid) ->", len(grid), " ONLY the first dimension")
print()
print("len(grid.shape) ->", len(grid.shape), " same as ndim, always")
print("np.size(grid, 0) ->", np.size(grid, 0), " size along one axis")
print()
print("len() is the trap: it never tells you the column count.")
Output:
grid is 3 rows by 4 columns
grid.shape -> (3, 4) the full picture, as a tuple
grid.size -> 12 total elements (3 * 4)
grid.ndim -> 2 how many dimensions
len(grid) -> 3 ONLY the first dimension
len(grid.shape) -> 2 same as ndim, always
np.size(grid, 0) -> 3 size along one axis
len() is the trap: it never tells you the column count.

shape, size, ndim and len().| Expression | Returns | Answers |
|---|---|---|
arr.shape | tuple, e.g. (3, 4) | How long is every dimension? |
arr.size | int, e.g. 12 | How many elements in total? |
arr.ndim | int, e.g. 2 | How many dimensions? |
len(arr) | int, e.g. 3 | How long is the first dimension only? |
len() is the one to be careful with. On a 3 by 4 array it returns 3 and says nothing whatsoever about the columns, so a silent bug is easy to write.
size is the product of the shape tuple, which is why an array built with np.zeros and one built with np.empty report the same size for the same dimensions.
Getting the shape of a Python list
Plain lists have no .shape at all, because it belongs to NumPy arrays:
import numpy as np
plain_list = [[1, 2, 3], [4, 5, 6]]
try:
plain_list.shape
except AttributeError as err:
print("plain_list.shape -> AttributeError:", err)
print()
print("two ways round it:")
print("np.shape(plain_list) ->", np.shape(plain_list))
print("np.array(plain_list).shape ->", np.array(plain_list).shape)
print()
print("with nothing but built-ins you have to measure each level yourself:")
print("len(plain_list) ->", len(plain_list), "(rows)")
print("len(plain_list[0]) ->", len(plain_list[0]), "(columns, assuming they all match)")
print()
print("rows of different lengths have no shape at all:")
try:
np.array([[1, 2], [3]])
except ValueError as err:
print("ValueError:", str(err).split(".")[0] + ".")
Output:
plain_list.shape -> AttributeError: 'list' object has no attribute 'shape'
two ways round it:
np.shape(plain_list) -> (2, 3)
np.array(plain_list).shape -> (2, 3)
with nothing but built-ins you have to measure each level yourself:
len(plain_list) -> 2 (rows)
len(plain_list[0]) -> 3 (columns, assuming they all match)
rows of different lengths have no shape at all:
ValueError: setting an array element with a sequence.
The fix is either np.shape(your_list), which converts on the fly, or np.array(your_list) if you want an array to keep working with.
Both need the rows to be the same length. A list like [[1, 2], [3]] is not rectangular, so there is no single tuple that could describe it and NumPy refuses to guess.
Shape of an image: height, width, channels
Images are where shape reading matters most, and where the order surprises people:
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(4, 3), dpi=100)
ax.plot([1, 2, 3], [2, 1, 3], lw=3, color="#0b6bcb")
ax.set_title("saved, then read back")
fig.savefig("chart.png")
plt.close(fig)
image = plt.imread("chart.png")
print("image.shape ->", image.shape)
h, w, channels = image.shape
print(f"height {h} pixels (shape[0], NOT the width)")
print(f"width {w} pixels (shape[1])")
print(f"channels {channels} (shape[2]: red, green, blue, alpha)")
print()
grey = image.mean(axis=2)
print("averaging the channels away leaves:", grey.shape)
print("one number per pixel, so the tuple loses its third entry")
Output:
image.shape -> (300, 400, 4)
height 300 pixels (shape[0], NOT the width)
width 400 pixels (shape[1])
channels 4 (shape[2]: red, green, blue, alpha)
averaging the channels away leaves: (300, 400)
one number per pixel, so the tuple loses its third entry
The height comes first. A 400 by 300 picture in everyday language has the shape (300, 400, 4), because rows are counted before columns.
The third entry is the channel count: 3 for RGB, 4 once an alpha channel is included, and no third entry at all for greyscale. Reducing the channels away drops the tuple back to two entries.
Changing the shape of an array
reshape rearranges the same values into a new set of dimensions:
import numpy as np
flat = np.arange(12)
print("flat.shape ->", flat.shape)
print("reshape(3, 4).shape ->", flat.reshape(3, 4).shape)
print("reshape(2, 6).shape ->", flat.reshape(2, 6).shape)
print()
print("-1 means 'work it out for me':")
print("reshape(-1, 4).shape ->", flat.reshape(-1, 4).shape)
print("reshape(3, -1).shape ->", flat.reshape(3, -1).shape)
print("reshape(-1, 1).shape ->", flat.reshape(-1, 1).shape, " the column scikit-learn asks for")
print()
print("the part that catches people out - reshape gives you a VIEW:")
original = np.arange(6)
view = original.reshape(2, 3)
view[0, 0] = 999
print("after writing 999 into the reshaped array, original is", original)
print()
print("resize() is different again. It changes the array in place,")
print("and it will invent zeros rather than complain:")
padded = np.arange(6)
padded.resize(3, 3)
print(padded)
Output:
flat.shape -> (12,)
reshape(3, 4).shape -> (3, 4)
reshape(2, 6).shape -> (2, 6)
-1 means 'work it out for me':
reshape(-1, 4).shape -> (3, 4)
reshape(3, -1).shape -> (3, 4)
reshape(-1, 1).shape -> (12, 1) the column scikit-learn asks for
the part that catches people out - reshape gives you a VIEW:
after writing 999 into the reshaped array, original is [999 1 2 3 4 5]
resize() is different again. It changes the array in place,
and it will invent zeros rather than complain:
[[0 1 2]
[3 4 5]
[0 0 0]]
-1 is a blank for NumPy to fill in from the total element count. reshape(-1, 1) turns a flat array into a single column, which is the form scikit-learn keeps asking for.
Notice what happened after the 999 was written in. reshape normally returns a view onto the original buffer rather than a copy, so edits travel both ways. Add .copy() when you need the two to stay independent.
resize behaves quite differently: it modifies the array in place and pads with zeros instead of raising, which can hide a mistake. For joining arrays instead of reshaping one, see np.concatenate and np.repeat.
Assigning to .shape is deprecated
Older tutorials set the shape by assignment. Recent NumPy versions warn about it:
import warnings
import numpy as np
arr = np.arange(6)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
arr.shape = (2, 3)
print("it still works ->", arr.shape)
for w in caught:
print()
print(f"{w.category.__name__}:")
print(str(w.message).replace("\n", " "))
print()
print("so write this instead:")
better = np.reshape(np.arange(6), (2, 3))
print("np.reshape(np.arange(6), (2, 3)).shape ->", better.shape)
Output:
it still works -> (2, 3)
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).
so write this instead:
np.reshape(np.arange(6), (2, 3)).shape -> (2, 3)

np.reshape, which returns a new view instead of mutating the array.It still runs, so nothing breaks today. The warning exists because assigning to .shape changes the array underneath any other code holding a reference to it, while np.reshape hands back a new view and leaves the original alone.
Common shape errors and what they mean
Three messages account for most shape problems:
import numpy as np
try:
np.arange(24).reshape(5, 5)
except ValueError as err:
print("1)", err)
print(" 24 will not divide into 25 slots. Check .size first.")
print()
try:
np.arange(24).reshape(-1, -1)
except ValueError as err:
print("2)", err)
print(" -1 is a single blank for NumPy to fill in.")
print()
try:
np.zeros((3, 4)) + np.zeros((5, 6))
except ValueError as err:
print("3)", err)
print(" Print both shapes before the arithmetic and the cause is obvious.")
print()
print("the shapes that DO broadcast share a dimension or have a 1 to stretch:")
print("(3, 4) + (4,) ->", (np.zeros((3, 4)) + np.zeros(4)).shape)
print("(3, 1) + (1, 4) ->", (np.zeros((3, 1)) + np.zeros((1, 4))).shape)
Output:
1) cannot reshape array of size 24 into shape (5,5)
24 will not divide into 25 slots. Check .size first.
2) can only specify one unknown dimension
-1 is a single blank for NumPy to fill in.
3) operands could not be broadcast together with shapes (3,4) (5,6)
Print both shapes before the arithmetic and the cause is obvious.
the shapes that DO broadcast share a dimension or have a 1 to stretch:
(3, 4) + (4,) -> (3, 4)
(3, 1) + (1, 4) -> (3, 4)

| Message | Cause | Fix |
|---|---|---|
| cannot reshape array of size 24 into shape (5,5) | The element counts do not match | Check arr.size against the product of your target shape |
| can only specify one unknown dimension | More than one -1 | Give a real number for all but one dimension |
| operands could not be broadcast together | Incompatible shapes in arithmetic | Print both shapes; add a 1 dimension or transpose |
| tuple index out of range | Indexing past ndim - 1 | Use shape[-1], or check ndim first |
The broadcast rule is easier than it sounds. Line the two tuples up from the right, and each pair must either match or contain a 1. That is also the rule behind element-wise work with np.add and comparisons using np.where.
Quick reference
arr.shapegives a tuple, one entry per dimension, and takes no parentheses.shape[0]is rows andshape[1]is columns on a 2D array;shape[-1]is always the innermost.(3,)is a one dimensional array and()is a single value.sizeis the total count,ndimis the number of dimensions,len()only sees the first one.- Lists need
np.shape(lst); the attribute does not exist on them. - Images read as
(height, width, channels), height first. reshapereturns a view; assigning to.shapeis deprecated.
Worth a look next if you are working with array dimensions:
Frequently asked questions
What does shape do in Python?
On a NumPy array, .shape reports the length of every dimension as a tuple. A 3 by 4 array returns (3, 4). It is an attribute, so you read it without parentheses.
What does .shape[0] mean?
The size of the first dimension. On a 2D array that is the number of rows, which is why range(data.shape[0]) loops once per row. On a 3D array it counts blocks instead.
What is the difference between shape[0] and shape[1]?
shape[0] is the row count and shape[1] is the column count of a 2D array. Rows are always measured before columns, which is also why image shapes give height before width.
Why does arr.shape() give a TypeError?
Because arr.shape is already a tuple and the parentheses try to call it. Drop them. If you want a function, np.shape(arr) is a real function and accepts lists too.
What is the difference between size and shape in NumPy?
shape is the tuple of dimension lengths and size is the single total number of elements, equal to the product of that tuple. A (3, 4) array has a size of 12.
How do I get the shape of a Python list?
Use np.shape(my_list), since lists have no .shape attribute. The rows have to be equal in length, otherwise there is no rectangular shape to report.
Why is my shape (3,) instead of (3, 1)?
(3,) is a one dimensional array and (3, 1) is a 2D array with a single column. Use arr.reshape(-1, 1) when a library wants the column form. The attribute itself is described in the numpy.ndarray.shape reference.

Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn