Skip to content
TaeyoungKim.dev

pandas loc vs. iloc: Why the First Filtered Row Raises KeyError

PythonWritten 3 min readTaeyoungKim
LinkedInX

A filtered table visibly has rows, yet df.loc[0] raises KeyError: 0. “Why is there no row zero when the first row is right there?” The answer is to separate a pandas index label from a row's current position.

loc reads labels; iloc reads positions

In the diagram, loc[20] and iloc[0] both select backup. loc[0] looks for a missing label; iloc[20] asks for a position outside the two-row result.

The argument to loc[...] is an index label. The argument to iloc[...] is an integer position in the current order, starting at zero. In a new DataFrame with default labels 0, 1, 2, they may happen to select the same row. Filtering usually retains the original labels, so that coincidence disappears.

Create three illustrative jobs and keep only the high-priority ones:

python
import pandas as pd

jobs = pd.DataFrame(
    {"job": ["build", "backup", "report"], "priority": ["low", "high", "high"]},
    index=[10, 20, 30],
)
urgent = jobs.loc[jobs["priority"] == "high"]
print(urgent)
print("first position:", urgent.iloc[0]["job"])
print("label 20:", urgent.loc[20]["job"])
text
       job priority
20  backup     high
30  report     high
first position: backup
label 20: backup

The first position is zero, but that row's label is 20. urgent.iloc[0] and urgent.loc[20] select it. urgent.loc[0] fails because label zero is absent; urgent.iloc[20] is out of bounds. The pandas loc documentation explicitly treats an integer argument as a label, not a position.

Distinguish an empty result from a wrong label

Replacing loc with iloc blindly can create a different error. If the filter returned zero rows, iloc[0] also fails, this time with IndexError. Print the row count and labels first:

python
print("rows:", len(urgent))
print("labels:", urgent.index.tolist())

if urgent.empty:
    print("No matching jobs")
else:
    print(urgent.iloc[0]["job"])

For this data, the output includes rows: 2, labels: [20, 30], and backup. A missing label zero is different from an empty result. Test both an input with high-priority jobs and one without them.

Renumber the index or keep the original labels?

If subsequent code only needs order within the filtered result, urgent.reset_index(drop=True) gives it new labels 0, 1. Then loc[0] selects the first row. But if 20 and 30 are identifiers linking back to the original jobs, dropping them loses that link.

python
renumbered = urgent.reset_index(drop=True)
print(renumbered.index.tolist())  # [0, 1]
print(renumbered.loc[0, "job"])  # backup

Use iloc when you mean position and loc when you mean a label or business identifier. With duplicate labels, loc[label] can even return multiple rows, so decide whether labels must be unique. Resetting the index is a change in its meaning, not merely a way to hide an error.

Key takeaways: first row is not the same as label zero

Filtering retains the original labels of surviving rows. To read the first row, confirm the result is nonempty and use iloc[0]; to read a known label, use loc[label]. Recognizing why the two coincide on a default index makes KeyError easier to diagnose.

Author

TaeyoungKim

Connecting technical foundations with implementation, verification, and production decisions.

#pandas#DataFrame#loc#iloc#KeyError

Read next