pandas shape — the (rows, columns) tuple behind every sanity check

df.shape returns (rows, columns) in one attribute — read it before you transform, assert it after.

It has no parentheses, no parameters, no docs page to speak of — and it is still the attribute I touch more than any other in pandas.

What it does

df.shape is a property, not a method: it returns the tuple (n_rows, n_columns) with zero arguments. It is implemented as len(self.index), len(self.columns) — two index-length lookups, no data scan. Series shape is a 1-tuple like (12,), empty frames report (0, 0), and a frame with only an index reports (5, 0). Because it is a plain attribute, calling df.shape() raises TypeError: 'tuple' object is not callable.

Why it matters

Every real pipeline has a shape assertion in it somewhere: after a filter, a join or a pivot you want (rows, cols) to confirm the transformation did what you think it did. shape[0] == 0 is also the honest emptiness test — a frame can be non-empty to len() and still hold no usable data, and shape distinguishes (0, 0) from (5, 0) at a glance. It is the first number you compare against the source system's row count when a load reports success and the numbers do not add up.

Examples

import pandas as pd

df = pd.DataFrame({
    'month': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
              'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
    'warehouse': ['Hamburg', 'Hamburg', 'Vienna', 'Vienna', 'Linz', 'Linz',
                  'Graz', 'Graz', 'Hamburg', 'Hamburg', 'Vienna', 'Vienna'],
    'units': [410, 380, 295, 512, 240, 331, 402, 288, 470, 505, 390, 610],
    'revenue': [8200, 7600, 5900, 10240, 4800, 6620, 8040, 5760, 9400, 10100, 7800, 12200],
})

print(df.shape)
(12, 4)

Always rows first, columns second — the same order as NumPy's ndarray.shape, which is exactly where the convention came from.

big = df[df['units'] > 500]

print(len(big), big.shape)
print(f"{df.shape[0]} rows x {df.shape[1]} cols -> size = {df.size}")
3 (3, 4)
12 rows x 4 cols -> size = 48

len() is shape[0] and nothing more. df.size is the product rows * columns — 48 cells here — which is the number to reach for when estimating memory or vectorized-loop cost.

none = df[df['revenue'] > 99999]
print(none.shape)
if none.shape[0] == 0:
    print("no rows matched - skip the summary")
(0, 4)
no rows matched - skip the summary

The guard I write most: filter first, check shape[0], then aggregate. (0, 4) still carries the four columns, so downstream code sees the schema it expects.

piv = df.pivot_table(index='warehouse', columns='month',
                     values=['units', 'revenue'], aggfunc='sum')
print(piv.shape)
print(piv.columns.names, piv.columns.nlevels)
(4, 24)
[None, 'month'] 2

Wide transforms are where shape earns its keep: 3 values x 12 months = 24 columns, and a two-level column MultiIndex. If the pivot shape looks wrong, you catch it one line later, not one report later.

print(df['units'].shape)

try:
    df.shape()
except TypeError as e:
    print("TypeError:", e)
(12,)
TypeError: 'tuple' object is not callable

One-dimensional data gets a one-tuple — and because shape is a property, parentheses are a bug, not a call. The REPL shows (12, 4) with no assignment; the tuple is already there, no print() needed.

Flags

FlagMeaning
(no args)A property — no parentheses, no parameters. df.shape() raises TypeError: 'tuple' object is not callable.
shape[0]Row count; the idiomatic emptiness guard is `if df.shape[0] == 0`. Identical to len(df).
shape[1]Column count; equals len(df.columns) and df.columns.size.
Series.shapeA 1-tuple like (12,) — handy in generic code: tuple length tells you the dimensionality you were handed.
df.sizeRows x columns as a single int (48 here); NaNs included — it counts cells, not values.
df.emptyTrue when there are NO rows; on an all-NaN frame, shape shows the data is there while empty still says False.
df.shape = (5, 5)Not assignable: property with no setter, unlike NumPy arrays — pandas raises AttributeError (after an unrelated UserWarning about attribute access).

Older than the changelog

shape has no whatsnew entry because it predates them: the whatsnew series starts at v0.4.x (September 2011), and the v0.4.0 tag of pandas/core/frame.py already ships it as `return (len(self.index), len(self.columns))` — the identical one-liner pandas 3.0 runs today. It came from NumPy, where ndarray.shape is one of the most fundamental attributes.

Property by design, docstring by committee

It stayed a cheap property on purpose: dimensionality must be free, or people would stop asserting it. The docstring's len() comparison is a recent addition — current docs explicitly contrast it: 'Unlike the len() method, which only returns the number of rows, shape provides both row and column counts.'

Two len() calls, fifteen years unchanged

The entire implementation in pandas 3.0's pandas/core/frame.py is `return len(self.index), len(self.columns)` — byte-for-byte the v0.4.0 line, just with a type annotation tuple[int, int] added. There is no C accelerator, no caching, none needed: axis lengths live in the Axis objects of the BlockManager, so the property is two Python-level len() reads. Series.shape does the same in generic.py: len(self._mgr.axes[0]). That is why shape is safe inside per-group loops where a data scan would be murder.

Fun facts

Pros

Cons

Takeaways