datarekha
Pandas & Data Wrangling Easy Asked at AmazonAsked at MicrosoftAsked at Walmart

What is the difference between loc and iloc in pandas, and when should you use each?

The short answer

loc selects rows and columns by label (index value or column name), while iloc selects by integer position. Use loc when your index carries meaningful labels like dates or IDs; use iloc for positional slicing regardless of what the index contains.

How to think about it

This is foundational, but the real trap is the default RangeIndex: when the index happens to be 0, 1, 2, …, loc and iloc look identical — until you sort or filter, and suddenly loc[0] and iloc[0] point at different rows. A strong answer names the two distinctions and the gotcha.

  • loclabel-based. Slice endpoints are inclusive. The “label” is whatever your index holds: strings, dates, integers.
  • ilocposition-based. Follows Python’s half-open [start, end) — end excluded.

A worked example — the inclusive/exclusive split

With a string index the two selectors clearly do different things, even when they return the same rows:

import pandas as pd

df = pd.DataFrame({"score": [90, 85, 78, 92, 67]},
                  index=["alice", "bob", "carol", "dave", "eve"])

print(df.loc["alice":"carol"])    # label slice — both ends INCLUDED
print(df.iloc[0:3])               # position slice — end EXCLUDED
       score
alice     90
bob       85
carol     78
       score
alice     90
bob       85
carol     78

Same three rows — but note why: loc["alice":"carol"] includes carol, while iloc[0:3] stops before position 3. To grab carol positionally you’d write iloc[0:3]; to grab her by label you name her and she’s included. (df.loc["bob", "score"] and df.iloc[1, 0] both give 85 — same cell, two addressing schemes.)

The RangeIndex trap

Here’s where it bites. With a default integer index, the inclusive vs exclusive difference alone already diverges the row counts, and a sort makes it worse:

df = pd.DataFrame({"val": [10, 20, 30, 40]})
print("loc[0:2] :", df.loc[0:2]["val"].tolist())     # label, inclusive
print("iloc[0:2]:", df.iloc[0:2]["val"].tolist())     # position, exclusive

df_sorted = df.sort_values("val", ascending=False)    # labels stay glued to rows
print("iloc[0]:", df_sorted.iloc[0].tolist())         # first row by position
print("loc[0]: ", df_sorted.loc[0].tolist())          # row whose LABEL is 0
loc[0:2] : [10, 20, 30]
iloc[0:2]: [10, 20]
iloc[0]: [40]
loc[0] : [10]

loc[0:2] returns three rows (labels 0, 1, 2 all included); iloc[0:2] returns two. And after the descending sort the labels travel with their rows, so iloc[0] is the new first row (40) while loc[0] is still the row labelled 0 (10) — they point at opposite ends. This is exactly why a loc[0] written against an unsorted frame silently returns the wrong row later. reset_index(drop=True) after a sort realigns labels to positions if you need them to agree.

Learn it properly Selection: loc vs iloc

Keep practising

All Pandas & Data Wrangling questions

Explore further

Skip to content