kmail.at
← learning

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.

2026-08-29 · 4 min read

$ 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: object

The 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     1615

Last 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     EU

Python 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-bounds

Slices 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: object

The 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])
True

head() 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

  1. 1Positions, not labels: iloc[0] is the first row even when the index says 10, 20, 30
  2. 2Slices exclude the stop position — the exact opposite of .loc's label slices
  3. 3Out-of-range slices clip silently; out-of-range single positions raise IndexError
  4. 4Rows first, then columns: the comma splits df.iloc[rows, cols]; reserve .iat for one cell
  5. 5If a column name exists, prefer .loc — keep iloc for first/last/Nth-by-position logic

Related commands