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

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" column
import 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: int64

2 .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:

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: object

3 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"] = True
import 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 warning

Rule: 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 byLabel (name)Integer position
Slice endInclusiveExclusive
Boolean mask✓ Yes✗ No
Exampledf.loc["bob", "age"]df.iloc[1, 0]

🎯 Mini Challenge: Precise Access

Use both indexers and a safe set operation on the same table.

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

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.