API edge cases and gotchas

pandas · Expert · 15 min

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] raises KeyError because [] uses labels when the index is integer. Use .iloc for positions, always. For non-integer indexes, positional s[0] was deprecated in 2.x and is label-only in 3.0.
  • Slicing semantics differ: s.loc[a:b] includes b, s.iloc[a:b] excludes it, and s[2:5] on a Series is positional (unless the index is float).
  • Alignment in assignment: df.loc[mask, "x"] = other_series aligns other_series by 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 s checks the index, not the values.
  • Missing keys vanish: groupby drops NaN keys by default (dropna=False keeps them), so group totals may not sum to the overall total. merge does 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 TypeError in 3.0; concat of 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 loc return a DataFrame instead of a row, and alignment explode into combinations.
  • Renamed or removed APIs: DataFrame.append (use concat), applymap (now DataFrame.map), fillna(method=) (use ffill/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 drift

Remember

  • Use .iloc for 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.

Check yourself

  1. `s = pd.Series([5, 6], index=[1, 0])`. What is `s[0]`?
  2. How do `groupby` and `merge` treat NaN keys by default?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pandas.pydata.org/docs/user_guide/gotchas.html