A DataFrame has two ways to locate a row at once: its label and its position.
Plain square brackets mix the two in confusing ways, so pandas provides loc and iloc to be explicit.
The loc selector accepts labels, lists of labels, label slices and Series of True and False.
The iloc selector accepts whole number positions, lists of positions and position slices.
Both take rows first and columns second, separated by a comma.
A colon on its own means everything, so sales.loc[:, "price"] is the whole price column.
Label slices include both ends, because a label has no obvious next label to stop before.
Position slices exclude the end, so iloc[0:3] gives positions 0, 1 and 2.
After filtering or sorting, labels and positions drift apart, and code that assumed they matched breaks.
Calling reset_index renumbers the rows from 0, which lines labels up with positions again when that is wanted.
Assigning through two selections in a row, such as sales[mask]["price"] = 0, can change a temporary copy and leave the table untouched.
Doing it in one step, as sales.loc[mask, "price"] = 0, changes the table itself.
A Series of True and False passed to loc acts as a row filter, which the next card covers.
In practice loc does most of the work, and iloc is used for things like taking the first few rows.
The usual mistake is assuming loc[0] means the first row, which only holds while the index runs 0, 1, 2.