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), 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)
Command Prompt showing the NumPy DeprecationWarning raised by assigning to the shape attribute
Prefer 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)
Command Prompt showing the NumPy cannot reshape array of size error and the operands could not be broadcast together error
The reshape and broadcast errors both name the shapes involved.
MessageCauseFix
cannot reshape array of size 24 into shape (5,5)The element counts do not matchCheck arr.size against the product of your target shape
can only specify one unknown dimensionMore than one -1Give a real number for all but one dimension
operands could not be broadcast togetherIncompatible shapes in arithmeticPrint both shapes; add a 1 dimension or transpose
tuple index out of rangeIndexing past ndim - 1Use 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.shape gives a tuple, one entry per dimension, and takes no parentheses.
  • shape[0] is rows and shape[1] is columns on a 2D array; shape[-1] is always the innermost.
  • (3,) is a one dimensional array and () is a single value.
  • size is the total count, ndim is 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.
  • reshape returns a view; assigning to .shape is 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.