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.
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.
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.
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.
| Flag | Meaning |
|---|---|
(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.shape | A 1-tuple like (12,) — handy in generic code: tuple length tells you the dimensionality you were handed. |
df.size | Rows x columns as a single int (48 here); NaNs included — it counts cells, not values. |
df.empty | True 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). |
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.
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.'
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.