loc vs iloc: Label vs Position
Reviewed & published by Brayan K
loc and iloc are the two precise indexers in pandas: loc selects rows and columns by their labels (names), while iloc selects them by their integer position — together they give you exact, unambiguous access to any cell or block of a DataFrame.
Part of the free Pandas course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Learn inclusive vs exclusive slicing, masking with .loc, safely setting values, and how to dodge the dreaded SettingWithCopyWarning.
What You'll Learn in This Lesson
- Select by label with df.loc[row, col]
- Select by position with df.iloc[row, col]
- loc slices are inclusive; iloc slices are exclusive
- Filter rows with a boolean mask inside .loc
- Safely set values with df.loc[...] = ...
- Avoid the SettingWithCopyWarning from chained indexing
1 .loc — Selection by Label
df.loc[row_label, col_label] selects using the names in the index and columns. To make the difference obvious, we'll use a custom string index.
import pandas as pd
df = pd.DataFrame({
"age": [25, 30, 35],
"city": ["NYC", "LA", "Chicago"],
}, index=["alice", "bob", "carol"])
df.loc["bob"] # the whole "bob" row (a Series)
df.loc["bob", "city"] # single cell -> "LA"
df.loc["alice":"bob"] # INCLUSIVE -> alice AND bob
df.loc[:, "age"] # all rows, the "age" columnimport pandas as pd
df = pd.DataFrame({
"age": [25, 30, 35],
"city": ["NYC", "LA", "Chicago"],
}, index=["alice", "bob", "carol"])
# Single cell by label
print(df.loc["bob", "city"]) # LA
# A label slice is INCLUSIVE of the end label
print(df.loc["alice":"bob"])
# One column across all rows
print(df.loc[:, "age"])
# alice 25
# bob 30
# carol 35
# Name: age, dtype: int64
# ✅ Expected output:
# LA
# age city
# alice 25 NYC
# bob 30 LA
# alice 25
# bob 30
# carol 35
# Name: age, dtype: int642 .iloc — Selection by Position
df.iloc[row_pos, col_pos] ignores labels entirely and uses integer positions, starting at 0 — exactly like indexing a Python list. Slices are exclusive of the end.
df.iloc[1] # second row by position (a Series)
df.iloc[1, 1] # row 1, column 1 -> "LA"
df.iloc[0:2] # EXCLUSIVE -> positions 0 and 1 only
df.iloc[:, 0] # all rows, first column
df.iloc[-1] # last row📏 The slice rule that catches everyone:
- df.loc["alice":"carol"] → 3 rows (end label included)
- df.iloc[0:3] → 3 rows; df.iloc[0:2] → 2 rows (end excluded)
import pandas as pd
df = pd.DataFrame({
"age": [25, 30, 35],
"city": ["NYC", "LA", "Chicago"],
}, index=["alice", "bob", "carol"])
# Position-based cell
print(df.iloc[1, 1]) # LA (row 1, col 1)
# A position slice is EXCLUSIVE of the end
print(df.iloc[0:2])
# Negative positions work like lists
print(df.iloc[-1])
# age 35
# city Chicago
# Name: carol, dtype: object
# ✅ Expected output:
# LA
# age city
# alice 25 NYC
# bob 30 LA
# age 35
# city Chicago
# Name: carol, dtype: object3 Masking and Setting Values with .loc
.loc accepts a boolean mask as its row selector, and — crucially — it's the safe way to set values. You can select rows by condition and a column to update in a single call.
# Read: rows where age > 28, just the "city" column
df.loc[df["age"] > 28, "city"]
# Write: give everyone over 28 a bonus flag in one statement
df.loc[df["age"] > 28, "senior"] = Trueimport pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Carol"],
"age": [25, 30, 35],
})
# Mask inside .loc to READ
print(df.loc[df["age"] >= 30, "name"])
# 1 Bob
# 2 Carol
# Name: name, dtype: object
# Mask inside .loc to WRITE (safe, in place)
df.loc[df["age"] >= 30, "tier"] = "senior"
df.loc[df["age"] < 30, "tier"] = "junior"
print(df)
# ✅ Expected output:
# 1 Bob
# 2 Carol
# Name: name, dtype: object
# name age tier
# 0 Alice 25 junior
# 1 Bob 30 senior
# 2 Carol 35 senior⚠️ The SettingWithCopyWarning Trap
This is the most infamous pandas gotcha. It happens when you chain two indexing operations and then assign — pandas can't tell whether you're editing the original or a temporary copy, so your change may silently vanish.
❌ Chained indexing (don't do this)
df[df["age"] > 30]["city"] = "Unknown" # SettingWithCopyWarning!
# The first [...] makes a copy; your assignment may NOT stick.✅ Single .loc call (do this)
df.loc[df["age"] > 30, "city"] = "Unknown" # one call, no warningRule: if you see two sets of square brackets back to back on the left side of an =, rewrite it as a single .loc[rows, cols] = value.
import pandas as pd
df = pd.DataFrame({
"product": ["Pen", "Notebook", "Eraser"],
"price": [1.50, 3.00, 0.75],
}, index=["p1", "p2", "p3"])
# 🎯 YOUR TURN:
# 1) Use .loc to get the price of "p2" by its label.
# 2) Use .iloc to get the price of the FIRST row by position.
# Replace the ___ blanks.
by_label = df.loc[___, "price"] # hint: the index label as a string
by_pos = df.iloc[___]["price"] # hint: an integer position
print(by_label) # 3.0
print(by_pos) # 1.5📋 Quick Reference
| Aspect | .loc | .iloc |
|---|---|---|
| Selects by | Label (name) | Integer position |
| Slice end | Inclusive | Exclusive |
| Boolean mask | ✓ Yes | ✗ No |
| Example | df.loc["bob", "age"] | df.iloc[1, 0] |
🎯 Mini Challenge: Precise Access
Use both indexers and a safe set operation on the same table.
- Grab the last two rows with .iloc
- Grab a label slice with .loc (notice it's inclusive)
- Use a masked .loc assignment to discount cheap items
import pandas as pd
df = pd.DataFrame({
"item": ["A", "B", "C", "D"],
"price": [10.0, 4.0, 8.0, 2.0],
}, index=["w", "x", "y", "z"])
# 1) last two rows by position
print(df.iloc[-2:])
# 2) inclusive label slice
print(df.loc["x":"y"])
# 3) safe masked assignment: cut prices under 5 by half
df.loc[df["price"] < 5, "price"] = df["price"] / 2
print(df)
# ✅ Expected output:
# item price
# y C 8.0
# z D 2.0
# item price
# x B 4.0
# y C 8.0
# item price
# w A 10.0
# x B 2.0
# y C 8.0
# z D 1.0❓ Frequently Asked Questions
Lesson complete — you have surgical control over your data!
You can select by label with .loc (inclusive slices), by position with .iloc (exclusive slices), filter with masks, set values safely, and avoid the SettingWithCopyWarning.
Practice quiz
What does .loc select by?
- Integer position
- Label (the index and column names)
- Data type
- Memory address
Answer: Label (the index and column names). loc selects by label — the names you see in the index and columns.
What does .iloc select by?
- Integer position, starting at 0
- Label
- Column dtype
- Sort order
Answer: Integer position, starting at 0. iloc uses integer positions like a Python list, starting at 0.
For an index ['alice','bob','carol'], how many rows does df.loc['alice':'bob'] return?
- 1
- 2
- 3
- 0
Answer: 2. loc slices are inclusive of the end label, so alice and bob = 2 rows.
How many rows does df.iloc[0:2] return?
- 3
- 1
- 0
- 2
Answer: 2. iloc slices are exclusive of the end, so positions 0 and 1 = 2 rows.
Which indexer accepts a boolean mask as its row selector?
- .loc
- .iloc
- Both equally
- Neither
Answer: .loc. loc accepts a boolean mask; iloc only takes integer positions.
What is the safe way to set values on filtered rows?
- df[mask]['col'] = x
- A for loop
- df.loc[mask, 'col'] = x
- df.iloc[mask] = x
Answer: df.loc[mask, 'col'] = x. A single df.loc[rows, cols] = value call edits the original safely.
What triggers the SettingWithCopyWarning?
- Using .loc once
- Chained indexing then assignment
- Reading a column
- Sorting the index
Answer: Chained indexing then assignment. Chaining two indexing ops (e.g. df[mask]['col'] = x) may edit a copy, raising the warning.
What does df.iloc[-1] return?
- The first row
- An error
- The last row by position
- The index name
Answer: The last row by position. Negative positions count from the end, like Python lists, so -1 is the last row.
For index ['alice','bob','carol'], how many rows does df.loc['alice':'carol'] return?
- 2
- 0
- 1
- 3
Answer: 3. loc end label is included, so alice, bob and carol = 3 rows.
Why must each condition in a multi-mask .loc use parentheses with & ?
- & binds tighter than comparison operators
- It runs faster
- loc requires double quotes
- Purely stylistic
Answer: & binds tighter than comparison operators. & has higher precedence than > or ==, so parentheses force the comparisons first.
Continue this course
- Previous: Filtering with Boolean Masks
- Next: SettingWithCopyWarning: Explained & Fixed — Why edits silently vanish, and the one-line .loc fix that stops it
- Quick reference: Pandas cheat sheet › Selecting & Filtering
Frequently asked questions
What is the core difference between loc and iloc?
loc selects by label (the index and column names you see), while iloc selects by integer position (0, 1, 2...). When the index is the default 0,1,2 range they often look identical, but they diverge the moment you have a custom index.
Why does df.loc[0:2] return three rows but df.iloc[0:2] return two?
loc slices are inclusive of the end label, so 0:2 returns labels 0, 1, and 2. iloc slices follow normal Python rules where the end position is exclusive, so 0:2 returns positions 0 and 1 only.
How do I avoid the SettingWithCopyWarning?
Never assign through chained indexing like df[df['a'] > 1]['b'] = 0. Do the whole operation in one .loc call: df.loc[df['a'] > 1, 'b'] = 0. This guarantees you modify the original DataFrame rather than a temporary copy.