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