Explicit selection avoids ambiguous indexing bugs and is the only reliable way to assign to a subset.
pandas has two explicit indexers, both taking [rows, columns]:
- `.loc` selects by label: index labels for rows, column names for columns.
- `.iloc` selects by integer position, like NumPy, regardless of labels.
Each position accepts a single item, a list, a slice or (for .loc) a boolean array:
df.loc["2024-01-03", "price"] # one value
df.loc[["a", "c"], ["price", "qty"]] # sub-table
df.iloc[0] # first row as a Series
df.iloc[:5, -2:] # first 5 rows, last 2 columnsOne important difference: `.loc` slices include the end label, while .iloc slices exclude the end position, as in Python. df.loc["a":"c"] includes row "c"; df.iloc[0:2] returns two rows. Label slicing works best on a sorted index.
Selecting a single row or column returns a Series; selecting with lists or slices returns a DataFrame. df.at[label, col] and df.iat[i, j] are faster accessors for exactly one value.
Plain df[...] is a shortcut with mixed rules: a string or list selects columns, a slice selects rows, a boolean Series filters rows. It's fine for those cases, but use .loc/.iloc whenever you select rows and columns together and always when assigning:
df.loc[df["qty"] == 0, "status"] = "out"A single .loc call both locates and sets in the original. Splitting it into two steps, df[df["qty"] == 0]["status"] = "out", is chained assignment: the first step produces a new object, so the original is never modified (pandas warns you).
Example
import pandas as pd
df = pd.DataFrame(
{"price": [9.5, 12.0, 4.25, 7.0], "qty": [2, 0, 5, 1]},
index=["a", "b", "c", "d"],
)
print(df.loc["b", "price"]) # 12.0
print(df.loc["a":"c", "qty"].tolist()) # [2, 0, 5] end label included
print(df.iloc[0:2].index.tolist()) # ['a', 'b'] end position excluded
print(df.iloc[-1, 0]) # 7.0
print(df.loc[["d", "a"], ["qty"]]) # DataFrame, rows in that order
df.loc[df["qty"] == 0, "status"] = "out" # one call: works
df.loc[df["qty"] > 0, "status"] = "ok"
print(df["status"].tolist()) # ['ok', 'out', 'ok', 'ok']
print(df.at["c", "qty"]) # 5 fast scalar accessRemember
.locis label-based,.ilocposition-based; both take [rows, cols].locslices include the end label;.ilocslices exclude it- Assign with one
.loc[row_selector, col] = valuecall .at/.iatare the fast way to read or write one cell
Watch out for
- Chained assignment such as
df[mask]['col'] = x, which never updatesdf.