Selecting: [], loc and iloc
In short: In pandas [] selects columns, loc selects rows and cells by label, and iloc selects them by position.
Problem
“The cheese is going up”
The supplier raises the price of white cheese from €7.90 to €8.40. The manager wants three things:
- a small table for the counter with only the name and the price;
- the price and stock of one product, without searching by eye;
- the new price in the table, not in some copy off to the side.
This is selecting: taking a part of the table, columns, rows or one cell, and sometimes changing it. pandas has three tools for it: [], loc and iloc.
The idea
Columns: []
You already take one column with []. If you put a list of names in the brackets, you get a table with those columns, in that order:
One name gives a Series with shape (25,). A list gives a DataFrame with shape (25, 2), even if the list has one column: products[['price']] is a table. The double brackets are not special syntax: the outer ones are for selecting, and the inner ones are for the list.
The idea
By label: loc
loc selects by labels: first the rows, then the columns, separated by a comma. With the names in the index this is “row White cheese, column price”:
- A row and a column give one cell: 7.9.
- A row alone gives the whole row as a Series. Its labels are now the names of the columns.
- Lists give a table: the cheese and the butter with price and stock.
See it
A slice with loc includes the end
A slice with a colon (start:stop) works in loc too, but there is a difference that confuses everyone:
loc[0:3] gives 4 names: from label 0 to label 3 inclusive. A slice of positions (iloc, Python lists) stops before the end and gives 3.
The reason is the labels. With names in the index, loc['Fresh milk':'Butter'] gives the dairy products, from the milk to the butter. Nobody would want the butter left out just because it is written last. How would you write “up to just after the butter”?
The idea
By position: iloc
iloc selects by position, like a NumPy array: the first row is 0, the last is -1. The labels play no part:
iloc[0, 1] is the first row, second column: “White bread”. iloc[-1, 1] is the last product: the gift card. iloc[:3, [1, 3]] is the first three rows, the columns in places 1 and 3.
Which one when? loc when you know what you are looking for: a product, a column by name. iloc when you care about the place: the first 5, the last row. Positions move as soon as the table is sorted or cut, while labels stay.
Try it
Coffee in the stockroom
Coffee is the product with id 17. How many packs are in stock? Put id in the index (by_id) and store the stock in coffee_stock as one number.
Solution
import pandas as pd
products = pd.read_csv('products.csv')
by_id = products.set_index('id')
coffee_stock = by_id.loc[17, 'stock']
print(coffee_stock)by_id = products.set_index('id') and coffee_stock = by_id.loc[17, 'stock'] gives 15. The trap is products.loc[17, 'stock']: without set_index the labels are the numbers from 0, label 17 is the 18th row, mint tea, and the answer is 33. Without the column (by_id.loc[17]) you get the whole row, not the number.
The idea
A copy or the same table
Now the new price. You cut out the dairy products and change the price of the cheese in them. Does products change?
No: in dairy the cheese is 8.4, but in products it is still 7.9. In pandas 3 every cut-out piece behaves as a copy. Changing the piece does not touch the original. This is called Copy-on-Write: pandas does not copy the data straight away, only when one of the two objects changes.
To change the table, you write into the table itself, with loc and the row and column at once: products.loc[6, 'price'] = 8.4.
See it
Two brackets in a row do not write
The temptation is products['price'][6] = 8.4: the column, then the cell. It looks like loc, but it is not:
The price is still 7.9. products['price'] is a separate object, a copy, and [6] = 8.4 writes into it. This is called chained assignment. pandas 3 never applies it to the table and shows a ChainedAssignmentError warning with a hint about .loc.
There is one rule: you write with one loc[row, column] = … on the table you want to change.
More precisely, if you are curious
In pandas 1 and 2 the same code sometimes changed the table and sometimes did not, depending on whether there was a copy or a view underneath. That is why there was a SettingWithCopyWarning, which said “maybe”. You will meet it in old articles and in answers around the internet. In pandas 3 (2026) the rule is clear, and the warning is history. If your code has to run on an old version too, loc in one step is correct there as well.
The idea
Many rows at once: a mask in loc
In the row place of loc you can put a mask: a column of True and False, like the masks in NumPy. Then the change applies to every row where the mask is True. Two products are out of stock and a delivery of 30 each arrives:
empty.sum() counts the empty ones: 2. After the loc line the banitsa and the cucumbers have 30 each. We will use masks on a table a lot in the next lesson, on filters.
Try it
A delivery for the nearly empty
Every product with fewer than 10 in stock gets 20 more. Store the mask in low, then change their stock in products.
Solution
import pandas as pd
products = pd.read_csv('products.csv')
low = products['stock'] < 10
products.loc[low, 'stock'] = products.loc[low, 'stock'] + 20
print(products.loc[low, ['name', 'stock']])low = products['stock'] < 10 catches 4 products, and products.loc[low, 'stock'] = products.loc[low, 'stock'] + 20 tops them up: the banitsa and the cucumbers become 20, the frying pan 28 and the knives 25. If you cut out short = products.loc[low] and change it, products stays the same. The chain products['stock'][low] = … does not write into the table either.
Exercise
The table for the counter
The names are in the index of by_name. Change the price of White cheese to 8.4 in by_name. Then store in counter the table for the counter: the dairy products from Fresh milk to Butter, with the columns price and stock.
Solution
import pandas as pd
products = pd.read_csv('products.csv')
by_name = products.set_index('name')
by_name.loc['White cheese', 'price'] = 8.4
counter = by_name.loc['Fresh milk':'Butter', ['price', 'stock']]
counterby_name.loc['White cheese', 'price'] = 8.4 writes into the table. counter = by_name.loc['Fresh milk':'Butter', ['price', 'stock']] gives 5 rows including the butter, because loc includes the end. With iloc[4:8] the butter is missing. If you change the price in counter or with by_name['price']['White cheese'], the counter and the stockroom show different prices, and by_name keeps the old one.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: The first table · Vectorization · Series and DataFrame
- Continues in: Filters and sorting · New columns · Types and missing values · Groups: groupby
- Tasks on this topic: Tables with pandas