pandas dtypes — read a DataFrame's column types in one line

One attribute, zero parentheses: df.dtypes is the X-ray of any DataFrame — read it before you trust a single calculation.

Merge keys that refuse to match, integers that came back floats — the culprit is almost always visible in one attribute: df.dtypes.

What it does

df.dtypes is an attribute, not a method: it returns a Series mapping every column name to its dtype, in column order, at zero cost. Its sibling df.select_dtypes(include=..., exclude=...) speaks the same vocabulary and grabs whole column groups — 'number' for math, 'datetime' for time logic. df.dtypes.value_counts() summarizes the schema in one line, and df.info() prints dtypes plus null counts when you want the full story. The attribute is read-only: changing types is astype()'s or the constructor's dtype= job.

Why it matters

Every downstream failure mode is a dtype failure in disguise: merge keys that don't match because one side loaded int64 and the other object, a units column that became float64 because one row was empty, numbers polluted by a stray 'N/A'. Reading dtypes immediately after every load takes one line and catches most of these before any logic runs. And the answer itself just changed: since pandas 3.0 (Jan 2026) text columns report str instead of object, so object now reliably means 'mixed types in here — go look'.

Examples

The default first check after building or loading a table — one attribute, every column's type in column order:
order_id      int64
city            str
units       float64
dtype: object

df.dtypes on a 3-column order table. order_id stayed int64, city is the new str text dtype (pandas 3.0 — PyArrow-backed when pyarrow is installed, python fallback otherwise), and units is float64: one empty value upcast the whole column. Verified on pandas 3.0.3.

On a 40-column import, the line-by-line view is noise. value_counts() turns dtypes into a one-line schema audit:
int64      1
str        1
float64    1
Name: count, dtype: int64

df.dtypes.value_counts(). If a column you expected to be int64 shows up under float64, you just caught a missing-value upcast; if something reports object on pandas 3.0, you caught mixed types. Verified.

Payroll table: sum only the numeric columns per row, without naming them. select_dtypes uses the same dtype vocabulary as dtypes:
   hours  rate_eur name  gross_eur
0     38      42.0  Ada       80.0
1     40      38.5   Bo       78.5
2     35      51.0   Cyd       86.0

pay["gross_eur"] = pay.select_dtypes("number").sum(axis=1) — 'number' catches int64 and float64 at once, no column lists. Check types with dtypes, then select by them. Verified.

CSV roundtrip: see what read_csv guessed before you compute on it:
sensor        str
temp_c    float64
dtype: object

pd.read_csv(io.StringIO("sensor,temp_c\nth-1,21.4\nth-2,19.8")).dtypes. The parser guesses types per column; when a guess is wrong, fix it at the boundary: pd.read_csv(f, dtype={"units": "Int64"}) keeps a mostly-int column integer even with gaps (verified).

Flags

FlagMeaning
df.dtypesattribute, not method — no parentheses; Series of column name → dtype in column order
df.dtypes.value_counts()one-line schema summary; the quickest audit after any load
df.select_dtypes(include='number')pick whole column groups by dtype ('number', 'datetime', object/str exclusions too)
df['col'].dtypeone column's dtype object — compare against dtype objects, not just display strings
df.convert_dtypes()re-infer best nullable dtypes: float-with-NaN → Int64, text → string
df.astype({'units': 'Int64'})cast per column; float-with-NaN → nullable Int64 works directly, no NaN cleanup
dtype= in read_csvfix types at the boundary, before the DataFrame exists — beats astype afterwards

There from the first release line

dtypes shipped with pandas' earliest public releases in the 0.x era of 2009 — per-column types were a founding design goal, inherited from R's data.frame, which (unlike a numpy 2-D array of a single dtype) lets every column carry its own type. The attribute itself never changed: what kept changing is the type system underneath it.

The system underneath was rebuilt three times

Nullable integer arrays arrived in 0.24 (Jan 25, 2019). pandas 1.0 (Jan 29, 2020) introduced pd.NA and a dedicated string dtype. pandas 2.0 (2023) added Arrow-backed StringDtype, and pandas 3.0 (Jan 21, 2026) made a str dtype the default for text — the first release where reading dtypes no longer shows object for plain text columns.

Why dtypes is instant: it reads the BlockManager

A DataFrame stores its columns as 1-D numpy (or Arrow) arrays grouped into blocks by dtype — one block for all int64 columns, one for floats, and so on, stacked inside a BlockManager. df.dtypes doesn't scan or infer anything: it walks the existing blocks and zips their dtype objects with the column names. That's also why heterogeneous frames cost memory — each distinct dtype means another block — and why operations mixing many dtypes force consolidation. The dtype you read in df.dtypes is literally the array's own dtype attribute.

Fun facts

Pros

Cons

Takeaways