Section 2 · pandas Foundations
Selecting with loc and iloc
18 min3 min readPython exercise
Lesson at a glance
Select columns with square brackets, then pick exact rows and columns with .loc (by label) and .iloc (by position), and know which one to use after sorting or filtering.
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Cartwheel’s product manager asks: “What do we charge for the coffee grinder, product 7?” and “Show me the five priciest products with their costs.” Both are selection problems: out of a table, get exactly these rows and these columns. pandas gives you two precise tools for that, .loc and .iloc, and most beginner confusion comes from mixing them up. The rule is short: .loc uses labels, .iloc uses positions.
Columns first
Square brackets on a DataFrame select columns:
import pandas as pd
products = pd.read_csv("products.csv", index_col="product_id")
print(products["name"].head(3)) # one column: a Series
print(products[["name", "unit_price"]].head(3)) # a list of columns: a DataFrameindex_col="product_id" made the product id the row index instead of the default 0, 1, 2. That single choice makes everything below read naturally: product 7 is the row labelled 7. On an already-loaded DataFrame, products.set_index("product_id") does the same thing and returns a new DataFrame.
You will also see products.unit_price in tutorials. Dot access works for simple names, but it fails for names with spaces, and it quietly returns the wrong thing when a column shares its name with a DataFrame method: a column called count or size is shadowed by the method of the same name. Square brackets always work, so make them your default.
The double brackets are not a typo. The outer pair is the selection; the inner pair is a Python list of column names. One column in a list still gives a DataFrame.
.loc: select by label
.loc[rows, columns] takes row labels first, then column labels:
import pandas as pd
products = pd.read_csv("products.csv", index_col="product_id")
print(products.loc[7, "unit_price"]) # one value
print(products.loc[7]) # one row, as a Series
print(products.loc[[11, 21, 37], ["name", "unit_price"]]) # chosen rows and columns
print(products.loc[1:3, "name":"unit_price"]) # label slicesLists pick exactly the labels you name, in that order. Slices take a range of labels, and here is the part to remember: .loc slices include the end label. products.loc[1:3] returns products 1, 2 and 3. The column slice "name":"unit_price" likewise includes unit_price. It makes sense once you think in labels: “from product 1 to product 3” includes product 3.
Ask for a label that does not exist, say products.loc[99], and you get KeyError: 99. That is a feature: a wrong id should fail loudly, not return an empty result you might paste into a report. When a missing label is a normal possibility, test first with 99 in products.index, which returns True or False.
.loc is also the one tool you use for assigning values, as you will see in Lesson 2.5: products.loc[7, "unit_price"] = 135.45 changes exactly one cell.
.iloc: select by position
.iloc ignores labels and counts positions from 0, with slices that exclude the end, exactly like Python lists:
import pandas as pd
products = pd.read_csv("products.csv", index_col="product_id")
print(products.iloc[0]) # first row, whatever its label
print(products.iloc[:3, :2]) # first three rows, first two columns
print(products.iloc[-1]["name"]) # last row's nameUse .iloc when position is what you mean: “the first five”, “the last row”, “every other row”. Use .loc when identity is what you mean: “product 7”, “the unit_price column”.
Why the difference matters: sorting
Sort the products by price and the two tools part ways:
import pandas as pd
products = pd.read_csv("products.csv", index_col="product_id")
by_price = products.sort_values("unit_price", ascending=False)
print(by_price.iloc[:3][["name", "unit_price"]]) # the three priciest
print(by_price.loc[1, "name"]) # still the skilletSorting moves rows but each row keeps its label. by_price.iloc[0] is now the Standing Desk, the most expensive item at 449 dollars; by_price.loc[1] is still the Cast Iron Skillet, wherever it ended up. Filtering behaves the same way: after you keep only some rows, labels have gaps (1, 4, 9…), and positions are renumbered 0, 1, 2. If you ever want fresh 0-to-n labels, reset_index(drop=True) gives them back.
In the exercise you answer the product manager’s questions with .loc and .iloc. Next you select rows by a condition instead of by label: “all orders over 200 dollars”, “all web orders in November”.
Key takeaways
df["col"]returns one column as a Series;df[["a", "b"]](a list inside the brackets) returns a DataFrame..loc[rows, columns]selects by label, and its slices include the end label..iloc[rows, columns]selects by position, and its slices exclude the end, like Python lists.- After sorting, labels stay attached to their rows, so
.locstill finds the same product while.iloc[0]finds whatever is now first. - Set a meaningful index such as
product_idwithindex_col=orset_index()so.locreads like a lookup.