DataLens

loc vs iloc

Select rows and columns precisely by label with .loc or by position with .iloc, and update values safely without the SettingWithCopyWarning.

Level: Foundations · 10 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

Plain square brackets in pandas do different things depending on what you pass: a name selects a column, a list selects columns, a slice selects rows, a mask filters rows. That's handy, but ambiguous. pandas has two explicit selectors that always mean the same thing:

Both take two parts separated by a comma: which rows, then which columns. Professional pandas code uses them for anything beyond a quick column lookup.

To see the difference clearly, use product names as the index:

import pandas as pd

products = pd.DataFrame({
    "name": ["French Press 600ml", "Arabica Gayo 250g", "Ceramic Mug", "Gel Pen Set", "USB-C Hub"],
    "category": ["Kitchen", "Coffee", "Kitchen", "Stationery", "Electronics"],
    "price": [185000, 95000, 55000, 38000, 289000],
    "stock": [24, 80, 150, 0, 15],
}).set_index("name")
print(products)
                       category   price  stock
name
French Press 600ml      Kitchen  185000     24
Arabica Gayo 250g        Coffee   95000     80
Ceramic Mug             Kitchen   55000    150
Gel Pen Set          Stationery   38000      0
USB-C Hub           Electronics  289000     15

.set_index("name") moves the name column into the index, so each row is labelled by its product.

.loc: by label

This module uses a small products table where each row is labelled with the product's name. .loc finds things by those labels: you say which row (or rows) and which column (or columns) you want, separated by a comma. Here's what you get back depending on how much you ask for:

print(products.loc["Ceramic Mug", "price"])
print(products.loc["Ceramic Mug"])
print(products.loc[["Gel Pen Set", "USB-C Hub"], ["price", "stock"]])
55000
category    Kitchen
price         55000
stock           150
Name: Ceramic Mug, dtype: object
              price  stock
name
Gel Pen Set   38000      0
USB-C Hub    289000     15

Use : to mean "all":

print(products.loc[:, "category"].tolist())
print(products.loc["Arabica Gayo 250g":"Gel Pen Set", "price"])
['Kitchen', 'Coffee', 'Kitchen', 'Stationery', 'Electronics']
name
Arabica Gayo 250g    95000
Ceramic Mug          55000
Gel Pen Set          38000
Name: price, dtype: int64
Watch out

Label slices with .loc include the end. "Arabica Gayo 250g":"Gel Pen Set" returns three products, Gel Pen Set included. Position slices (with .iloc or lists) exclude the end. This is the most common loc/iloc mix-up.

.loc with a boolean mask

.loc accepts a mask for the rows, which lets you filter and pick columns in one step:

low = products.loc[products["stock"] < 20, ["category", "stock"]]
print(low)
                category  stock
name
Gel Pen Set   Stationery      0
USB-C Hub    Electronics     15

This is the idiomatic way to write "these columns, for rows where…".

.iloc: by position

Sometimes you don't care what a row is called, only where it is: the first row, the last row, the top ten. That's what .iloc is for. It ignores the labels completely and counts positions from 0, exactly like a Python list:

print(products.iloc[0])
print(products.iloc[-1, 1])
print(products.iloc[1:3, [0, 2]])
category    Kitchen
price        185000
stock            24
Name: French Press 600ml, dtype: object
289000
                  category  stock
name
Arabica Gayo 250g   Coffee     80
Ceramic Mug        Kitchen    150

Use .iloc when position is what you mean: "the first 10 rows", "the last row", "every other row". Use .loc when you mean a specific thing by name. Code that says iloc[3] when it means "the Gel Pen Set" breaks the moment the data is sorted differently.

When the index is 0, 1, 2…

With the default numbered index, .loc and .iloc look the same, which is a trap:

sales = pd.read_csv("sales.csv")
completed = sales[sales["status"] == "completed"]
print(completed.index[:5].tolist())
print(completed.loc[22, "product"])
print(completed.iloc[0]["product"])
[0, 1, 2, 3, 4]
Pour-over Kettle
Desk Organizer

After filtering, the index labels keep their original values (they're labels, not positions). completed.loc[22] means "the row labelled 22", while completed.iloc[0] means "the first remaining row". If you want fresh 0, 1, 2… labels after filtering, call .reset_index(drop=True).

Changing values safely

.loc is also how you set values, including for rows matching a condition:

products.loc["Gel Pen Set", "stock"] = 40
products.loc[products["category"] == "Kitchen", "price"] = (
    products.loc[products["category"] == "Kitchen", "price"] * 0.9
).round().astype(int)
print(products[["category", "price", "stock"]])
                       category   price  stock
name
French Press 600ml      Kitchen  166500     24
Arabica Gayo 250g        Coffee   95000     80
Ceramic Mug             Kitchen   49500    150
Gel Pen Set          Stationery   38000     40
USB-C Hub           Electronics  289000     15

Every Kitchen price is now 10% lower, and the pen set is restocked, in place.

The SettingWithCopyWarning

You'll eventually see this warning. It comes from chained selection followed by assignment:

products[products["stock"] == 0]["stock"] = 50     # SettingWithCopyWarning, may do nothing
products.loc[products["stock"] == 0, "stock"] = 50  # correct: one .loc, rows and column together

The first line selects some rows (making a new, temporary DataFrame) and then sets a value in that temporary copy, so your real data may not change at all. pandas warns you because it can't tell whether you meant to change the original. The fix is always the same: do the selection and assignment in one .loc call. If you genuinely want a separate DataFrame to modify, make it explicit with .copy(): low_stock = products[products["stock"] < 20].copy().

Why this matters for clean code

.loc and .iloc make your intent unambiguous: "this product's price" versus "the third row". Use .loc for meaning and filtering, .iloc for position, a single .loc[mask, column] = value for updates, and .copy() when you want an independent subset. Code written this way doesn't silently stop working when the data is sorted, filtered or re-indexed.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: Adding, renaming, and dropping columns →