PD-01-008-P · Primer — loc vs iloc
The idea in one sentence
loc selects rows and columns by their labels (the names in the index and header), while iloc selects them by their integer position (0, 1, 2…), just like counting cells in a spreadsheet.
Why it exists
A DataFrame has two ways to point at a cell: you can name it ("the row labeled b, the column named temp") or you can count to it ("the first row, the second column"). Those are genuinely different questions, so pandas gives you two genuinely different tools instead of overloading one bracket that guesses your intent. loc answers "which label?" and iloc answers "which position?" — keeping them separate means your code says exactly what you mean, stays correct even after the data is sorted or filtered, and never silently grabs the wrong cell. Once you internalize "l = label, i = integer," the whole selection system clicks into place.
How it works
Think of every DataFrame as having invisible name tags on each row (the index) and each column (the header). loc[row_label, col_label] walks to the cell by reading those name tags, so df.loc['b', 'temp'] means "row named b, column named temp." iloc[row_pos, col_pos] ignores names entirely and counts from zero, so df.iloc[0, 1] means "row 0, column 1." Both accept a single value, a list, or a slice — and slicing is where they differ in a way worth memorizing (see the gotcha). Reach for loc when you know the meaningful name; reach for iloc when you only care about order, like "the first three rows."
import pandas as pd
df = pd.DataFrame(
{'city': ['Cairo', 'Lima', 'Oslo'], 'temp': [35, 22, 8]},
index=['a', 'b', 'c'],
)
# loc = by LABEL: row 'b', column 'temp'
print(df.loc['b', 'temp']) # 22
# iloc = by POSITION: row 0, column 1
print(df.iloc[0, 1]) # 35
# loc slice is INCLUSIVE of the end label
print(list(df.loc['a':'b'].index)) # ['a', 'b']
# iloc slice is EXCLUSIVE of the end position
print(list(df.iloc[0:2].index)) # ['a', 'b']
The one gotcha
The two slicers do not stop at the same place. iloc follows normal Python rules — iloc[0:2] gives positions 0 and 1, stopping before 2. But loc is inclusive of the end label — loc['a':'b'] returns rows a and b. This trips up nearly every beginner: with iloc you exclude the end, with loc you include it. If your slice unexpectedly has one extra (or one missing) row, this asymmetry is almost always the reason.
See it — same slice request, two different endpoints
flowchart TB src["DataFrame index<br/>(a, b, c)"] --> loc["loc['a':'b']<br/>label slice<br/>INCLUSIVE end"] src --> iloc["iloc[0:2]<br/>position slice<br/>EXCLUSIVE end"] loc --> lr["result rows:<br/>a, b"] iloc --> ir["result rows:<br/>a, b"] lr --> note["same 2 rows here,<br/>but loc keeps 'b'<br/>by NAME, iloc stops<br/>BEFORE position 2"] ir --> note
The one-line takeaway
loc = by label (end included); iloc = by integer position (end excluded) — pick by which one you actually know.
Where you'll meet it
Every time you pull a value out of a DataFrame: grabbing one customer's row by ID (loc), peeking at the first five rows during exploration (iloc[:5]), selecting a named block of columns, or setting a cell with df.loc[mask, 'col'] = value. You'll also hit it the moment a tutorial's df[0] fails — the fix is almost always choosing loc or iloc on purpose.
Named concepts: loc label-based, iloc position-based, row/column selection, slicing
