Iterating: items, iterrows, itertuples
Loop over a DataFrame by column (items), by row as a Series (iterrows) or by row as a namedtuple (itertuples) — what each returns, their traps, and when a loop is acceptable.
Coming from Python lists, the first instinct is to write a for loop over the rows. Pandas does let you loop — notebook 08 shows items(), iterrows() and itertuples() — but each one returns something different, and each has traps. This topic explains all three; the next topic shows why you should usually not loop at all.
text
player runs avg
0 Virat 741 61.75
1 Rohit 417 32.08
2 Bumrah 12 6.00Definition
Iterating a DataFrame means visiting its columns or rows one at a time in a Python
forloop. Pandas provides three generators:
items()yields(column_name, column_Series)pairs — one per column.iterrows()yields(index_label, row_Series)pairs — one per row.itertuples()yields one namedtuple per row, with the index first and fields named after the columns.
- Generator — values are produced one at a time as the loop asks for them (
next(df.iterrows())gives the first one). Notebook 08 shows this withnext(). - Row as a Series (iterrows) — a Series can hold only one dtype, so mixed columns are converted to a common type.
- namedtuple (itertuples) — a light tuple whose fields you read with dots:
t.player,t.runs. Original dtypes are kept.
Plain for col in df: loops over column names, and for v in df["runs"]: loops over the values of one column.
Syntax
python
for col_name, col in df.items(): ...
for idx, row in df.iterrows(): ...
for t in df.itertuples(index=True, name="Pandas"): ...
for idx, value in series.items(): ...| Method / parameter | Meaning | Default |
|---|---|---|
df.items() | yields (column label, Series) | — |
df.iterrows() | yields (index label, Series of that row) | — |
df.itertuples(index=...) | True → first field is Index; False → no index field | True |
df.itertuples(name=...) | name of the namedtuple type; None → plain tuples | "Pandas" |
series.items() | yields (index label, value) | — |
Example — items(): column by column
python
import pandas as pd
df = pd.DataFrame({
"player": ["Virat", "Rohit", "Bumrah"],
"runs": [741, 417, 12],
"avg": [61.75, 32.08, 6.0],
})
for col_name, col in df.items():
print(col_name, "->", col.tolist())text
player -> ['Virat', 'Rohit', 'Bumrah']
runs -> [741, 417, 12]
avg -> [61.75, 32.08, 6.0]How it works: items() is the cheapest loop — a DataFrame usually has few columns, and each col is already a Series, so you can use fast Series methods inside the loop.
Example — iterrows(): row by row as a Series
python
for idx, row in df.iterrows():
print(idx, row["player"], row["runs"])
idx, row = next(df.iterrows())
print(row)text
0 Virat 741
1 Rohit 417
2 Bumrah 12
player Virat
runs 741
avg 61.75
Name: 0, dtype: objectHow it works: each row is a Series whose index is the column names and whose name is the row label (0). Notice the dtype: object — the row mixes text and numbers, so pandas stores it as generic objects.
Trap 1 — dtypes are not preserved. With only numeric columns, the row becomes one common numeric type:
python
_, r = next(df[["runs", "avg"]].iterrows())
print(r)
print(type(r["runs"]))text
runs 741.00
avg 61.75
Name: 0, dtype: float64
<class 'numpy.float64'>runs was an integer column, but inside the loop it is 741.0, a float — because avg forced the whole row to float64.
Trap 2 — changing row does not change the DataFrame.
python
d3 = df.copy()
for idx, row in d3.iterrows():
row["runs"] = 0
print(d3["runs"].tolist())
for idx, row in d3.iterrows():
d3.loc[idx, "runs"] = row["runs"] + 1
print(d3["runs"].tolist())text
[741, 417, 12]
[742, 418, 13]row is a copy. To write back you must use d3.loc[idx, "runs"] = ... — slow, and almost always replaceable by d3["runs"] = d3["runs"] + 1.
Example — itertuples(): fast, dtype-safe rows
python
for t in df.itertuples():
print(t)
for t in df.itertuples(index=False, name="Player"):
print(t.player, t.runs, t.avg)text
Pandas(Index=0, player='Virat', runs=741, avg=61.75)
Pandas(Index=1, player='Rohit', runs=417, avg=32.08)
Pandas(Index=2, player='Bumrah', runs=12, avg=6.0)
Virat 741 61.75
Rohit 417 32.08
Bumrah 12 6.0How it works: each row is a namedtuple. runs is still an integer (741, not 741.0). Field access uses dots: t.runs. It is many times faster than iterrows because no Series is built per row.
Trap 3 — column names that aren't valid identifiers (spaces, starting with a digit) are renamed to positional names:
python
d2 = pd.DataFrame({"player name": ["Virat"], "1st": [5]})
print(next(d2.itertuples()))text
Pandas(Index=0, _1='Virat', _2=5)Comparison — the three loops
items() | iterrows() | itertuples() | |
|---|---|---|---|
| Iterates over | columns | rows | rows |
| Each step gives | (name, Series) | (index, Series) | namedtuple (Index, col1, col2, …) |
| Access a field | col.mean() | row["runs"] | t.runs |
| Keeps dtypes? | ✅ yes | ❌ no (row upcast to common type) | ✅ yes |
| Speed | fast (few columns) | slowest | much faster than iterrows |
| Writing back | via df[col_name] = ... | must use df.loc[idx, col] | must use df.loc[t.Index, col] |
When is looping OK?
- Calling something outside pandas for each row: sending an email, calling an API, writing a file per team.
- Very small DataFrames where clarity matters more than speed.
- Logic where each row depends on the previous result (and
cumsum/shiftcan't express it).
For everything else — arithmetic, labels, string cleaning, look-ups — use vectorised operations (next topic).
Common mistakes
- ❌
for row in df:expecting rows. ✅ That loops over column names; useiterrows()/itertuples()for rows. - ❌ Modifying
rowinsideiterrows()and expectingdfto change. ✅ Write withdf.loc[idx, col]— or better, vectorise. - ❌ Trusting dtypes inside
iterrows(). ✅ Useitertuples()when types matter. - ❌
t["runs"]on an itertuples row. ✅t.runs(ort[2]by position).
Interview tip
Q: Why is itertuples() faster than iterrows()?
A: iterrows() builds a brand-new pandas Series (with an index and a common dtype) for every row, which is expensive. itertuples() builds a lightweight namedtuple straight from the column arrays, keeping the original dtypes — far less overhead per row.
Key points to remember
- items() loops over columns and yields (column name, Series) pairs.
- iterrows() yields (index, row Series); the row is upcast to a common dtype, so ints can become floats.
- itertuples() yields namedtuples (Index, col1, ...) — faster than iterrows and keeps dtypes; access with t.col.
- for x in df loops over column NAMES, not rows.
- Changing the row inside iterrows does not change the DataFrame — write back with df.loc[idx, col].
- Column names with spaces or leading digits become _1, _2 ... in itertuples.
- Loop only for side effects, tiny data, or truly sequential logic — otherwise vectorise.
Try it yourself
items, iterrows and itertuples
The dtype and write-back traps
Practice: print a scorecard line per player
Revision Flashcards
Tap a card to flip it and see the answer.
