Pandas loc vs iloc
.loc selects rows and columns by label (index values and column names), while .iloc selects by integer position (0, 1, 2...). The rule of thumb: loc = "what it's called," iloc = "where it sits."
What Is the Difference Between loc and iloc?
loc looks up the index labels you pass; iloc counts positions from zero regardless of what the index contains. On a default RangeIndex they look identical, which is exactly how bugs sneak in later.
import pandas as pd
df = pd.DataFrame(
{"price": [10, 20, 30], "qty": [5, 3, 8]},
index=["apple", "banana", "cherry"],
)
df.loc["banana", "price"] # 20 (by label)
df.iloc[1, 0] # 20 (row 1, column 0)
Why Are loc Slices Inclusive?
loc slices include both endpoints, while iloc slices follow normal Python rules and exclude the stop value. This is the single most common source of off-by-one surprises.
df.loc["apple":"banana"] # 2 rows: apple AND banana included
df.iloc[0:1] # 1 row: apple only (stop excluded)
Labels can't be "one past the end" the way integers can, so pandas made label slicing inclusive on purpose.
How Do Boolean Masks Work with loc?
Pass a boolean Series to loc to filter rows, optionally combined with a column selection in one step. iloc does not accept a boolean Series aligned by label โ use loc for masks.
df.loc[df["price"] > 15] # rows where price > 15
df.loc[df["price"] > 15, "qty"] # just the qty column
df.loc[(df["price"] > 15) & (df["qty"] > 5), ["price", "qty"]]
How Does loc Avoid SettingWithCopyWarning?
Chained indexing like df[df.price > 15]["qty"] = 0 writes to a temporary copy, so pandas raises SettingWithCopyWarning and your data may not change. A single loc call with row and column selectors assigns directly to the original DataFrame.
# Bad โ two operations, writes to a copy:
df[df["price"] > 15]["qty"] = 0 # SettingWithCopyWarning
# Good โ one loc call, one operation:
df.loc[df["price"] > 15, "qty"] = 0 # works, no warning
Under pandas 3.0's copy-on-write behavior the chained version silently does nothing, so fixing it now is not optional.
Common Pitfalls
- Integer labels are ambiguous: if your index is
[3, 1, 2],df.loc[1]finds the label 1, butdf.iloc[1]returns the second row (label 1... by coincidence or not). Never guess โ check the index. - Slice endpoints:
loc["a":"c"]includes "c";iloc[0:2]excludes row 2. - KeyError vs IndexError: a missing label raises
KeyErrorfromloc; an out-of-range position raisesIndexErrorfromiloc. - Plain
df[...]is inconsistent: it slices rows with a slice but selects columns with a string. Be explicit withloc/ilocin production code.
Once rows are selected, aggregating them is usually the next step โ see pandas groupby for that half of the workflow.
Pro Tip: For reading or writing a single cell, use df.at["banana", "price"] (label) or df.iat[1, 0] (position). They only handle scalars, which makes them noticeably faster than loc/iloc inside loops.