pandas · difficulty ◆
pandas loc[] — label-based selection and honest updates
Select by label, slice with both endpoints included, and write back the way pandas actually endorses.
You have typed df.loc[...] a thousand times without knowing it was born to kill pandas' most infamous guessing game.
$ df.loc[]What it does
df.loc[] is the label-based indexer: give it row labels and column names and it hands back exactly those rows and columns. It accepts a single label, a list of labels, a label slice — note that df.loc['b':'d'] includes the stop label, unlike every other Python slice — a boolean mask aligned to the index, or a callable. The comma splits axes: df.loc[rows, cols]. Combine mask and column list — df.loc[df.region == 'EU', 'units'] — and you get filtered selection in one expression, which is also the form the pandas team endorses for writing values back.
Why it matters
Selection is the most frequent thing you do in pandas, and .loc is the spelling that never surprises you: a missing label raises KeyError instead of guessing, and the slice endpoint is right there in your code. Its second job is quieter but bigger: under the Copy-on-Write default in pandas 3.0, chained assignment like df[mask]['col'] = x writes to a temporary copy that vanishes — I ran it on 3.0.3 and the frame did not change. df.loc[mask, 'col'] = x is the one assignment form that works and will keep working.
Example
$ df.loc['Tokyo']units 210
revenue 3990
region APAC
Name: Tokyo, dtype: objectOne label returns that row as a Series; columns become the index.
$ df.loc[['Berlin', 'Tokyo'], ['units', 'revenue']] units revenue
Berlin 120 2400
Tokyo 210 3990Label lists pick rows and columns in the exact order you pass them.
$ df.loc['Paris':'Vienna'] units revenue region
Paris 85 1530 EU
Tokyo 210 3990 APAC
Vienna 95 1615 EULabel slices INCLUDE the stop label — unlike every other Python slice.
$ df2.loc[df2.units < 100, 'region'] = 'EU-small'
df2 units revenue region
Berlin 120 2400 EU
Paris 85 1530 EU-small
Tokyo 210 3990 APAC
Vienna 95 1615 EU-smallMask plus column is the endorsed pattern for conditional updates.
$ df.loc[df.region == 'EU', 'units'].sum()np.int64(300)Filter then aggregate in one chain — the daily-driver pattern.
$ df.loc['Prague']KeyError: 'Prague'Missing labels raise — .loc never guesses, which is exactly why it is safe.
$ df.loc[lambda d: d.units > 100] units revenue region
Berlin 120 2400 EU
Tokyo 210 3990 APACA callable indexer keeps selection inside method chains.
$ df.loc['Tokyo', 'revenue']np.int64(3990)Two scalars give a scalar back — but df.at[] does it with less machinery.
Common flags
- df.loc['Tokyo']
- single label returns one row as a Series
- df.loc[['a', 'b'], ['x', 'y']]
- label lists for rows and columns, order preserved
- df.loc['a':'c']
- label slice, stop label INCLUDED — and .loc[-1] is still a KeyError
- df.loc[df.units > 100]
- boolean mask, the filtering workhorse
- df.loc[mask, 'col'] = v
- the endorsed write-back form; survives Copy-on-Write
- df.loc['new'] = row
- assigning an unseen label appends an enlargement row
- df.loc[lambda d: ...]
- callable indexer — selection inside a method chain
History
Formalized in pandas 0.11 (April 22, 2013)
The 0.11.0 release notes introduced .loc and .iloc as the two explicit indexers — '.loc is strictly label based, will raise KeyError when the items are not found' — and in the same breath listed irow, icol, and iget_value as headed for deprecation. .loc was the designated successor to the .ix guessing game from day one.
The .ix era ends (0.20 in 2017, removal in 1.0)
.ix tried to guess labels versus positions and was deprecated in 0.20.0 (May 2017). Its removal in 1.0.0 (January 2020) left .loc and .iloc as a deliberate pair: label-based versus position-based, no guessing. Copy-on-Write (default since 2.0, the only behavior in 3.0) then made df.loc[mask, 'col'] = x the single sanctioned write-back form.
Fun facts
Pros & cons
pros
- + Handles labels, lists, slices, masks, and callables with one syntax
- + The only write-back form that survives Copy-on-Write without warnings
- + Raises KeyError on missing labels — explicit beats clever
cons
- − Label slices including the stop label surprise people coming from Python lists
- − Scalar .loc access is slower than .at — extra machinery you rarely need for one cell
- − One indexer with many input types means many subtle rules to internalize
Takeaways
- 1df.loc[rows, cols] — always rows first, columns second; missing labels raise KeyError, which is a feature
- 2Label slices include the stop label: df.loc['a':'c'] gives you 'a', 'b', AND 'c'
- 3Write back with df.loc[mask, 'col'] = x — chained df[mask]['col'] = x silently fails under Copy-on-Write
- 4Scalar single lookups deserve df.at['row', 'col'] — .loc works but does more work than needed
- 5df.loc[len(df)] = row appends to default-indexed frames — fine for scripts, prefer pd.concat in modern code