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:
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"]) job priority
20 backup high
30 report high
first position: backup
label 20: backupThe 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:
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.
renumbered = urgent.reset_index(drop=True)
print(renumbered.index.tolist()) # [0, 1]
print(renumbered.loc[0, "job"]) # backupUse 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.

