100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Python

Indexing with loc and iloc

Master the two primary pandas selection accessors — .loc for label-based indexing and .iloc for integer-position-based indexing — and the pitfalls of chained indexing.

Data Selection & IndexingBeginner9 min readJul 8, 2026
Analogies

Indexing with loc and iloc

Pandas offers two explicit accessors for selecting rows and columns: .loc, which selects by label (the actual index/column names), and .iloc, which selects by integer position (0-based, like a Python list), regardless of what the labels actually are. Both accept a row selector and, optionally, a column selector separated by a comma — df.loc[row_selector, col_selector] — and both can take a single value, a list, a slice, or a boolean array in either position. Using the explicit accessor rather than plain bracket indexing (df[...]) removes ambiguity about whether you mean a label or a position, which matters a great deal once a DataFrame's index is not the default 0..n-1 range (for example, after filtering, sorting, or setting a custom column as the index).

🏏

Cricket analogy: .loc selects a player by their actual name label like 'Kohli', while .iloc selects whoever sits in batting position 3 regardless of who that is, which matters once the lineup has been re-sorted by average and no longer matches the original order.

.loc: Label-Based Selection

.loc selects using the actual index and column labels. A crucial and often-surprising detail is that slicing with .loc is inclusive of both endpoints — df.loc[2:5] includes the row labeled 5, unlike Python's normal slicing convention (and unlike .iloc, which excludes the stop). .loc also accepts boolean arrays for filtering, making df.loc[df['price'] > 100, 'product'] a common and idiomatic pattern for selecting specific columns from filtered rows in one call.

🏏

Cricket analogy: df.loc[2:5] on an over-by-over log includes the row labeled over 5, not just up to over 4, and df.loc[df['runs'] > 100, 'batter'] idiomatically pulls just the batter names from overs where 100+ runs were scored.

python
import pandas as pd

df = pd.DataFrame({
    'product': ['Widget', 'Gadget', 'Gizmo', 'Doohickey'],
    'price': [19.99, 250.00, 12.75, 305.50],
}, index=[10, 20, 30, 40])

print(df.loc[10:30])                 # inclusive of label 30
print(df.loc[df['price'] > 100, 'product'])
# 20      Gadget
# 40    Doohickey
# Name: product, dtype: object

.iloc: Position-Based Selection

.iloc ignores the actual labels entirely and selects purely by 0-based integer position, following standard Python slicing rules where the stop value is exclusive. This makes .iloc the right tool when you want 'the first 5 rows' or 'the last column' regardless of what the index labels happen to be — for instance df.iloc[0:5, -1] always means the first five rows of the last column, whereas the equivalent .loc call would depend entirely on what the actual labels are.

🏏

Cricket analogy: df.iloc[0:5, -1] on a scorecard always means the first five overs of the last recorded column, regardless of whether those overs are labeled 1-5 or, after a rain-delay reorder, labeled entirely differently.

python
import pandas as pd

df = pd.DataFrame({
    'product': ['Widget', 'Gadget', 'Gizmo', 'Doohickey'],
    'price': [19.99, 250.00, 12.75, 305.50],
}, index=[10, 20, 30, 40])

print(df.iloc[0:2])          # first two rows, position-based, stop excluded
print(df.iloc[-1, -1])       # last row, last column -> 305.5
print(df.iloc[[0, 3], 0])    # rows at positions 0 and 3, first column

A simple mnemonic: '.loc' rhymes with 'location label', and '.iloc' starts with an 'i' for 'integer'. If you ever need to look up by what a row is called, use .loc; if you need to look up by where a row physically sits, use .iloc.

Chained indexing like df[df.price > 100]['product'] = 'X' (selecting twice, then assigning) can trigger pandas' SettingWithCopyWarning and may silently fail to modify the original DataFrame, because the first selection can return a copy rather than a view. The fix is to combine both selections into a single .loc call: df.loc[df.price > 100, 'product'] = 'X'.

  • .loc selects by label; .iloc selects by 0-based integer position — the two are not interchangeable.
  • .loc slicing is inclusive of the stop label; .iloc slicing is exclusive of the stop position, following standard Python rules.
  • Both accessors support combined row and column selection: df.loc[rows, cols] / df.iloc[rows, cols].
  • .loc accepts boolean arrays directly, enabling df.loc[condition, 'col'] in one step.
  • Chained indexing (two separate bracket operations) risks the SettingWithCopyWarning; prefer a single combined .loc/.iloc call for assignment.
  • Plain bracket indexing df[...] is ambiguous about label vs. position once the index isn't the default RangeIndex — explicit accessors remove that ambiguity.

Practice what you learned

Was this page helpful?

Topics covered

#Python#PandasNumPyStudyNotes#DataScience#IndexingWithLocAndIloc#Indexing#Loc#Iloc#Label#SQL#StudyNotes#SkillVeris