← pandas·Lesson 5·13 min·0/4 exercises

[] vs .loc vs .iloc

Three ways in, three different questions — and the one that silently returns the wrong row.

df["col"].loc.iloc.at.iatlabel slicesposition slices

Watch it happen

Play it through, or step back and forth yourself.

df
city
cups
mon
Delhi
120
tue
Mumbai
80
wed
Pune
150
thu
Delhi
95

pandas gives you three bracket forms, and they answer different questions. df[...] is a shortcut with its own rules, .loc works by label, and .iloc works by position.

The idea

This is the most-confused topic in pandas, and it isn't because it's hard — it's because three similar-looking things answer three different questions.

[] selects columns

orders["cups"]              # a Series — one column
orders[["city", "cups"]]    # a DataFrame — a list of columns

That's the odd one. For every other indexable object in Python, brackets pick an element; here they pick a column. It's a shortcut for the common case, and it has two exceptions — a boolean mask filters rows, and a slice takes rows. Those exceptions are exactly why bare brackets get confusing, so use them for columns and masks and nothing else.

.loc works by label

df.loc["tue"]              # the row LABELLED tue
df.loc["tue", "cups"]      # row first, then column
df.loc["mon":"wed"]        # three rows — the end IS included
df.loc[df.cups > 100, "cups"]   # filter and column, in one go

Two things to internalise. Rows come first, columns second. And a .loc slice includes its endpoint — unlike every other slice in Python. That's deliberate: with labels there's no "one past the end" to name, so excluding the end would make label slicing nearly useless.

.iloc works by position

df.iloc[1]        # the second row
df.iloc[1, 1]     # row 1, column 1
df.iloc[0:2]      # two rows — the end is EXCLUDED
df.iloc[-1]       # the last row

This one behaves like an ordinary Python index: zero-based, negatives from the end, end excluded.

Hand-drawn notes showing that square brackets select columns while loc and iloc select rows first, one by label and one by position.

Where it bites

As long as your index is words, mixing them up throws an error and you fix it. The danger is when the labels are integers — which is what filtering leaves behind, since the original 0, 1, 2… labels come along with their rows:

busy = orders[orders["cups"] > 100]
busy.index          # [0, 3, 5, 7, 8, 10] — gappy

busy.loc[3]         # the row LABELLED 3
busy.iloc[3]        # the FOURTH row
busy[3]             # KeyError — no column called 3

The first two both work and return different rows. No exception, no warning. That's the bug that survives code review, and it's why the advice is always to be explicit.

Hand-drawn notes showing that a loc slice includes its final label while an iloc slice excludes its final position, like an ordinary Python slice.

Which to reach for

  • Labels mean something — dates, ids, names — use .loc.
  • You genuinely mean position — "the first five rows" — use .iloc.
  • Columns or a boolean mask — bare [] is fine and reads well.

And for a single cell in a loop, .at and .iat are the fast paths — same label-versus-position split, much less overhead, one cell only.

Practice

Write it yourself. The answer is there when you want it.

Putting the kettle on…

Starting up…

Write it yourself

not graded

Keep the orders over 100 cups and print the index labels that came with them — they are gappy. Print the city at loc[3] next to the city at iloc[3], and say which is which. Then set the index to date and print the shape of a loc slice across two days against an iloc slice of two rows. Finish with one .loc taking the rows over 120 cups and the city and cups columns.

Write something and press Run — the output appears here.

Your turn

4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.

Return the city of the row labelled 2 in orders, using .loc.

your answer

Return the first three rows of orders by position.

your answer

Return just the city and cups columns for the rows where cups is over 120 — in a single .loc call.

your answer

Return the last row of orders, by position.

your answer