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.
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.
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'.
.locselects by label;.ilocselects by 0-based integer position — the two are not interchangeable..locslicing is inclusive of the stop label;.ilocslicing 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]. .locaccepts boolean arrays directly, enablingdf.loc[condition, 'col']in one step.- Chained indexing (two separate bracket operations) risks the SettingWithCopyWarning; prefer a single combined
.loc/.iloccall 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
1. Given `df.loc[2:5]`, which rows are included in the result?
2. What does `df.iloc[0:3]` select?
3. Which accessor should you use to select 'the last row and last column' regardless of the actual index labels?
4. Why is `df[df.price > 100]['product'] = 'X'` considered risky?
5. What is the safe, recommended way to assign 'X' to the product column only where price > 100?
Was this page helpful?
You May Also Like
DataFrame Basics
Understand the structure of a pandas DataFrame — rows, columns, index, and dtypes — and the core operations for creating, inspecting, and selecting from tabular data.
Boolean Filtering
Learn how to filter DataFrame rows using boolean masks built from comparison and logical operators, including combining conditions and using isin and query for cleaner syntax.
Series Basics
Learn the anatomy of a pandas Series — its values, index, and name — and how label-based alignment, vectorized operations, and dtype inference make it more than a plain list.
Setting and Resetting the Index
Understand how to promote columns to a DataFrame's index with set_index and revert them back with reset_index, and why the index matters for alignment.