pandas · difficulty ◆
pandas iloc[] — select rows and columns by integer position
When you need the 3rd row — not the row labeled 3 — iloc[] is the tool, and its slices behave exactly like Python's.
Your index reads 10, 20, 30 and you want the first row. Typing df[0] there is how pandas careers get cut short.
$ df.iloc[]What it does
df.iloc[] selects purely by integer position: df.iloc[0] is the first row, df.iloc[-1] the last, df.iloc[2, 1] the cell at row position 2, column position 1 — regardless of what the index labels say. It accepts an int, a list of ints, a slice, a boolean array, or a callable, and the comma splits the axes: rows first, columns second. Unlike .loc, its slices follow plain Python rules — the stop position is excluded, negative positions count from the end, and oversized slices simply clip.
Why it matters
Real data rarely has tidy labels: after a filter or a sort, row labels are arbitrary leftovers, but positions are always meaningful — 'first three rows', 'last row', 'every Nth record'. iloc is the deterministic way to grab those, immune to duplicate labels and unreadable datetime stamps. It also backstops the methods you already use: df.head(n) is literally df.iloc[:n], df.tail(n) is df.iloc[-n:], and df.iat[r, c] is its stripped-down single-cell variant. And when an int-labeled index makes df.loc[0] and df.iloc[0] look identical, only one of them is safe to retype from memory — knowing which prevents the classic off-by-one data disaster.
Example
$ df.iloc[-1]units 95
revenue 1615
region EU
Name: Vienna, dtype: objectThe last row — no len(df) needed. One position returns a Series with the columns as its index.
$ df.iloc[-3:, [0, 1]] units revenue
Paris 85 1530
Tokyo 210 3990
Vienna 95 1615Last three rows, columns by position 0 and 1. The comma splits rows from columns.
$ df.iloc[2, 1]
df.iat[2, 1]np.int64(3990)
np.int64(3990)Cell at row position 2, column position 1 — .iat is the lean single-cell sibling.
$ df.iloc[::-1].head(2) units revenue region
Vienna 95 1615 EU
Tokyo 210 3990 APAC::-1 reverses the row order; head(2) then keeps the former bottom rows. Any Python slice works.
$ df.iloc[:-2]
df.iloc[0:10] units revenue region
Berlin 120 2400 EU
Paris 85 1530 EU
units revenue region
Berlin 120 2400 EU
Paris 85 1530 EU
Tokyo 210 3990 APAC
Vienna 95 1615 EUPython slice semantics: :-2 drops the last two rows, and 0:10 silently clips instead of raising.
$ df.iloc[4]IndexError: single positional indexer is out-of-boundsSlices clip, single positions raise — an asymmetry worth memorizing.
$ dfm = df.set_axis([10, 20, 30, 40])
dfm.iloc[0]
dfm.loc[10]units 120
revenue 2400
region EU
Name: 10, dtype: object
units 120
revenue 2400
region EU
Name: 10, dtype: objectThe trap: with an int-labeled index both lines print the same rows. dfm.iloc[0] means 'first row'; dfm.loc[10] means 'row labeled 10'. They agree here only by coincidence.
$ df.head(2).equals(df.iloc[:2])Truehead() is iloc slicing with better PR — verified on pandas 3.0.5.
Common flags
- df.iloc[3]
- single position returns one row as a Series; raises IndexError if out of range
- df.iloc[[0, 2]]
- list of positions, order preserved — [2, 0] puts row 2 first
- df.iloc[1:4]
- Python slice: start included, stop EXCLUDED — opposite of .loc label slices
- df.iloc[:, 0]
- all rows, first column — the second axis sits after the comma
- df.iloc[2, 1]
- row and column positions together return a scalar; .iat[2, 1] is the fast path
- df.iloc[::-1]
- negative steps reverse the rows — no sort_index() needed
- df.iloc[lambda d: [-1]]
- callable indexer, handy inside method chains
History
Formalized in pandas 0.11 (April 22, 2013)
The 0.11.0 release notes introduced .iloc as the strictly position-based indexer — the explicit twin of .loc — and announced both as replacements for the older irow()/icol() helpers and the label-or-position guesser .ix. From day one the plan was a clean pair: labels go to .loc, positions go to .iloc, nothing left to infer.
One design decision, two slicing cultures
iloc's slices exclude the stop position because positions should feel like Python lists; .loc's label slices include the stop label because a named range should include what you named. Granting each indexer its own honest semantics pushed all the ambiguity into .ix, which was deprecated in 0.20 (2017) and removed in 1.0 (2020).
Fun facts
Pros & cons
pros
- + Deterministic: positions never shift meaning with index labels, duplicates don't matter
- + Speaks fluent Python: negative indices, steps, and clip-on-overflow slices just work
- + Selection logic that survives filters and sorts — 'last row' is always the last row
cons
- − Magic numbers in code — df.iloc[:, 3] tells the reader nothing that a column name would
- − Silent slice clipping can hand you fewer rows than expected when the frame shrank upstream
- − Position-based logic breaks the moment someone reorders or drops rows upstream
Takeaways
- 1Positions, not labels: iloc[0] is the first row even when the index says 10, 20, 30
- 2Slices exclude the stop position — the exact opposite of .loc's label slices
- 3Out-of-range slices clip silently; out-of-range single positions raise IndexError
- 4Rows first, then columns: the comma splits df.iloc[rows, cols]; reserve .iat for one cell
- 5If a column name exists, prefer .loc — keep iloc for first/last/Nth-by-position logic