These edge cases cause silent wrong answers; recognising them is what separates expert pandas users.
A tour of behaviours that regularly bite experienced users:
- Integer labels vs positions. With an integer index such as
[10, 20, 30],s[0]raisesKeyErrorbecause[]uses labels when the index is integer. Use.ilocfor positions, always. For non-integer indexes, positionals[0]was deprecated in 2.x and is label-only in 3.0. - Slicing semantics differ:
s.loc[a:b]includesb,s.iloc[a:b]excludes it, ands[2:5]on a Series is positional (unless the index is float). - Alignment in assignment:
df.loc[mask, "x"] = other_seriesalignsother_seriesby label first. Missing labels produce NaN, not an error. - Boolean truthiness:
if df["x"] > 0:raises "truth value is ambiguous"; use.any()/.all().x in schecks the index, not the values. - Missing keys vanish:
groupbydrops NaN keys by default (dropna=Falsekeeps them), so group totals may not sum to the overall total.mergedoes match NaN keys with each other, unlike SQL. - dtype drift: inserting NaN into an int column makes it float; setting a string into a float column used to silently upcast to object, warns since 2.1 and raises
TypeErrorin 3.0;concatof int with missing columns gives float. - `inplace=True` rarely saves memory, returns None, and with copy-on-write doesn't propagate through selections.
- Duplicates in the index make
locreturn a DataFrame instead of a row, and alignment explode into combinations. - Renamed or removed APIs:
DataFrame.append(useconcat),applymap(nowDataFrame.map),fillna(method=)(useffill/bfill), frequency aliases such as"M"(now"ME"). - `value_counts` sorts by count, and ties keep first-seen order; don't rely on alphabetical order.
The common thread: be explicit. Use .loc/.iloc, name your join keys and validate them, pass dropna=, observed= and dtypes deliberately, and check shapes and dtypes after each important step.
Example
import numpy as np
import pandas as pd
s = pd.Series([1, 2, 3], index=[10, 20, 30])
try:
s[0]
except KeyError:
print("s[0] is a label lookup -> KeyError; use s.iloc[0] =", s.iloc[0])
df = pd.DataFrame({"k": ["a", np.nan, "a"], "v": [1, 2, 3]})
print(df.groupby("k")["v"].sum().sum(), df["v"].sum()) # 4 6 NaN key dropped
print(df.groupby("k", dropna=False)["v"].sum().sum()) # 6
left = pd.DataFrame({"key": [np.nan, "x"], "l": [1, 2]})
right = pd.DataFrame({"key": [np.nan, "y"], "r": [9, 8]})
print(len(left.merge(right, on="key"))) # 1 NaN matched NaN (SQL would not)
d = pd.DataFrame({"x": [1, 2, 3]})
d.loc[[0, 2], "y"] = pd.Series([100, 300], index=[2, 0]) # aligned by label!
print(d["y"].tolist()) # [300.0, nan, 100.0]
ints = pd.Series([1, 2, 3])
ints.loc[1] = np.nan
print(ints.dtype) # float64: dtype driftRemember
- Use
.ilocfor positions;[]with an integer index means labels - groupby drops NaN keys, merge matches them: pass
dropna=explicitly - Assignment from a Series aligns by label before writing
- Watch for dtype drift and removed APIs when upgrading
Watch out for
- Upgrading pandas without running tests with warnings enabled, missing deprecations that turn into behaviour changes.