Skip to content

pandas iloc

Select rows and columns by integer position, counting from zero.

.iloc is the positional counterpart to .loc. They answer different questions: .loc asks which label?, .iloc asks which row number? On a freshly loaded data frame those happen to coincide, because the default index runs 0, 1, 2, … and position matches label. That coincidence is temporary, and mistaking it for a rule is where the trouble starts.

Official documentation: DataFrame.iloc

The forms worth knowing

df.iloc[0]              # first row
df.iloc[-1]             # last row
df.iloc[:5]             # first five rows
df.iloc[0, 1]           # row 0, column 1 -- a single cell
df.iloc[:5, :2]         # first five rows, first two columns
df.iloc[[0, 2, 4]]      # rows 0, 2 and 4

Note the comma. df.iloc[0, 1] selects a cell in one operation; df.iloc[0][1] selects a row and then indexes into it — slower, harder to read, and on assignment it fails silently. Use the comma form. The same rule applies to .loc.

Unlike .loc, an .iloc slice excludes its endpoint, exactly like ordinary Python slicing: df.iloc[:3] gives rows 0, 1 and 2. The inconsistency is deliberate — .iloc counts positions, so it behaves like a list.

.iloc takes numbers, never pd.col expressions. For a condition, you want .loc.

Any index but the default breaks the correspondence

The moment your index carries meaning, position and label stop being the same idea at all. Group the Global Coal Plant Tracker by country and the index becomes country names:

import pandas as pd

url = ('https://www.bambooweekly.com/content/files/wp-content/uploads/2024/02/'
       'global-coal-plant-tracker-january-2024.xlsx')

by_country = (
    pd.read_excel(url, sheet_name='Units', usecols=['Country', 'Capacity (MW)'])
    .groupby('Country')['Capacity (MW)']
    .sum()
    .sort_values(ascending=False)
)

by_country.iloc[0]         # 2326616.3  -- whatever sits first
by_country.loc['China']    # 2326616.3  -- the row named China
by_country.loc[0]          # KeyError: 0

.loc[0] is not merely wrong here — it is meaningless, because no label 0 exists in this index. Any set_index, groupby, resample, value_counts or pivot_table puts you in this situation, which covers most real analyses.

The subtler case: sorting

When the index is still the default, sorting produces a quieter version of the same problem. Labels stay glued to their rows, so both forms remain legal and simply disagree.

Netflix published an engagement report listing every title watched over six months. Bamboo Weekly #45 used it. Sort alphabetically, then ask for "the first row" two ways:

url = ('https://www.bambooweekly.com/content/files/4cd45et68cgf/1HyknFM84ISQpeua6TjM7A/'
       '97a0a393098937a8f29c9d29c48dbfa8/'
       'what_we_watched_a_netflix_engagement_report_2023jan-jun.xlsx')

df = (
    pd.read_excel(url, skiprows=5, usecols=['Title', 'Hours Viewed'])
    .sort_values('Title')
)

df.iloc[0, 0]           # '#Alive // #살아있다'          -- row now in position 0
df.loc[0, 'Title']      # 'The Night Agent: Season 1'  -- the row labelled 0

Both work, neither raises, and they return different titles. Look at the index and it becomes obvious:

df.iloc[:3]
                                    Title  Hours Viewed
1927                      #Alive // #살아있다      10700000
8499        #AnneFrank - Parallel Stories        800000
12506  #AtFirstSight // #Sohavégetnemérös        200000

Rows 0, 1 and 2 by position — carrying labels 1927, 8499 and 12506. Before the sort they agreed. Afterwards they have nothing to do with each other, and nothing warns you.

Three mistakes people make

Treating .iloc and .loc as interchangeable. They coincide only on an untouched default index. Set an index, group, resample or sort, and they part company — sometimes loudly with a KeyError, sometimes silently with the wrong row.

Chained brackets: df.iloc[0]['Title']. This selects a row and then indexes into the result. For reads it merely wastes time; for writes it fails silently, because the assignment lands on a temporary object that is discarded. Say df.iloc[0, 0] or df.loc[0, 'Title'] in one step.

Expecting an .iloc slice to include its endpoint. It does not. .iloc[:3] returns three rows, while .loc['a':'c'] includes c. The two follow different rules on purpose.

Watch it

How .loc and .iloc treat Pandas slices differently covers the endpoint difference and where it bites.

Practice it

Work through an .iloc exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/iloc/

Go deeper

Bamboo Weekly is the practice. If you want the structured version — full courses with downloadable Jupyter notebooks, plus live Pandas office hours when you get stuck — that is LernerPython+Data. A paid Bamboo Weekly subscription is included with it.

See it on real data

Below are the 49 Bamboo Weekly exercises that use iloc on real-world data — try each one, then study the worked solution.

Part of the Pandas Methods Index. See also practice by skill.