Indexing and Selection
Selecting rows and columns the safe way
.loc and .iloc are the two selection dialects — label-based and position-based — plus boolean masks and .at/.iat for scalars; the 'chained assignment' pitfall lurks for the unwary.
What you will learn
- Use .loc for labels and .iloc for positions
- Filter with boolean masks
- Avoid chained-assignment mutations
What is this?
df.loc[rows, cols] selects by label/index values; df.iloc[...] selects by integer position. Boolean masks (df[df['revenue'] > 10]) filter rows, and explicitly as df.loc[mask, cols] to set values. The setter-with-chained-indexing warning is real: setting through df[a][b] = x is undefined behavior.
Why do you need it?
- Label vs position confusion is the single most common pandas bug.
- .loc['2026-01':] slices a time index by label — impossible with positions.
- Safe assignment via .loc prevents the infamous CopyOnWrite warnings and silent drops.
- Writing selection as .loc[rows, cols] is readable and future-proof.
Mental model
Calling a person by name vs pointing to their seat number
.loc vs .iloc
Names (labels) are stable and semantic; seats (positions) are quick but change when the room is rearranged. Pick by what you mean, not what's convenient.
How it works
- df.loc['2026-03-14', ['revenue']] select with label arguments in row, column order.
- df.iloc[0, 2] selects the first row's third column by position.
- Slices in .loc are inclusive of both ends (label semantics); .iloc slices are exclusive like Python.
- Boolean mask df[mask] returns rows where mask is True; mask can be built from any column comparison.
- df.at[label, col] / df.iat[position] fetch scalars fastest.
Choosing the selector.
Examples
Selection dialects in action
import pandas as pddf = pd.DataFrame({"user": ["a","b","c"], "rev": [10, 20, 30]}, index=[2, 5, 8])print(df.loc[5]) # the row labeled 5print(df.iloc[0]) # the first row (position)big = df.loc[df['rev'] > 15] # maskdf.loc[big.index, 'rev'] = df.loc[big.index, 'rev'] * 1.1 # safe setprint(df)Labels like 5 and positions like 0 are different concepts — .loc[5] picks the index-labeled row while .iloc[0] picks whatever sits first. Setting via .loc avoids chained-assignment surprises.
In production
- A timeseries dashboard slices df.loc['2026-03':] for the current month by label.
- An ETL job updates only null cells: df.loc[df['x'].isna(), 'x'] = fallback.
- A partition merge keys rows to frames by user_id labels rather than position.
Best practices
- Prefer .loc/.iloc over plain df[...] for anything beyond a simple column.
- Assign through a single .loc call; never df[a][b] = v.
- Set df.index name (e.g., 'date') and use label slices generously.
Common mistakes
- Writing df[0] expecting the first row — that's a column key error.
- Slicing .loc[0:2] and assuming it stops at position 2 — it INCLUDES label 2.
- Setting with chained indexing and getting a SettingWithCopyWarning (or silent no-op).
Debugging tips
- Reading a SettingWithCopyWarning? Rewrite the setter as a single .loc call.
- If selection returns empty, print df.index — the labels don't match what you assumed.
- For duplicate index labels, .loc returns a DataFrame, not a Series — a common type surprise.
Interview checkpoint
Hands-on lab
- Practice .loc vs .iloc on a non-default index
- Filter with masks and assign safely
- Catch the chained-assignment trap
Selection Championship
25 min · intermediate
Quick quiz
With index [2,5,8], df.iloc[1] returns the row labeled:
.loc slices include both endpoints; .iloc slices exclude the stop like Python.