NumPy Shape in Python: shape[0], shape[1] and the Tuple

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.

Diagram of NumPy shape tuples for 1D, 2D and 3D arrays, with shape[0] labelled as rows and shape[1] as columns
One entry in the shape tuple for every dimension of the array.

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
Command Prompt showing that the NumPy shape attribute returns a tuple object
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]
Command Prompt output showing shape[0] as the row count and shape[1] as the column count of a NumPy array
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.

ExpressionOn a 2D arrayOn a 3D array
shape[0]rowsblocks
shape[1]columnsrows
shape[2]IndexErrorcolumns
shape[-1]columnscolumns

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)
Command Prompt showing TypeError tuple object is not callable after calling the NumPy shape attribute with parentheses
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.
Command Prompt comparing NumPy shape, size, ndim and len on the same 3 by 4 array
The same array measured four ways: shape, size, ndim and len().
ExpressionReturnsAnswers
arr.shapetuple, e.g. (3, 4)How long is every dimension?
arr.sizeint, e.g. 12How many elements in total?
arr.ndimint, e.g. 2How many dimensions?
len(arr)int, e.g. 3How 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