at & iat: Fast Single Values
Read or write exactly one cell with .at (by label) and .iat (by position) — the faster scalar versions of loc and iloc.
Watch the lesson
1 video on CodeWithMunnaX
Often you need just one cell: "how many wickets did Bumrah take?", "update Dhoni's runs to 200". loc and iloc can do this, but they are general-purpose tools that first work out whether you asked for a row, a slice, a list or a mask. Pandas has two specialised accessors for the single-cell case — .at and .iat — covered in Munna's video 05.
Definition
DataFrame.at[row_label, column_label]gets or sets a single scalar value by labels, andDataFrame.iat[row_position, column_position]does the same by integer positions. They are the scalar-only, lighter-weight counterparts oflocandiloc.
- Single scalar value — exactly one cell. No lists, no slices, no masks:
at/iatraise an error if you try. - By labels / by positions — the same split as
loc/iloc: at ↔ loc, iat ↔ iloc. - Lighter-weight — because they skip all the "what kind of selector is this?" checks, they are a bit faster. The gain only matters when you read/write cells thousands of times in a loop.
Syntax
python
df.at[row_label, "col"] # read one value by label
df.at[row_label, "col"] = value # write one value by label
df.iat[i, j] # read one value by position
df.iat[i, j] = value # write one value by position
series.at[label], series.iat[i] # Series versions take one key| Accessor | Row key | Column key | Returns |
|---|---|---|---|
.at | index label | column name | one scalar |
.iat | row position (int) | column position (int) | one scalar |
.loc | label / list / slice / mask | name / list / slice | scalar, Series or DataFrame |
.iloc | int / list / slice | int / list / slice | scalar, Series or DataFrame |
Example — reading single values
python
print(players.at[0, "player"])
print(players.iat[0, 0])
print(players.at[3, "wickets"], players.iat[3, 4])
p = players.set_index("player")
print(p.at["MS Dhoni", "strike_rate"])
print(p.iat[2, 4])text
Virat Kohli
Virat Kohli
20 20
220.5
220.5How it works: on the default index, label 0 and position 0 are the same row, so at[0, "player"] and iat[0, 0] agree. Row 3 is Jasprit Bumrah, and wickets is column position 4 (player 0, team 1, role 2, runs 3, wickets 4). With players as the index, at uses the name, while iat[2, 4] counts positions in the new table: row 2 is Dhoni, and column 4 is now strike_rate (because player left the columns to become the index).
Example — writing single values
python
p.at["MS Dhoni", "runs"] = 200
print(p.loc["MS Dhoni", "runs"])
p.iat[0, 2] = 750
print(p.head(2))text
200
team role runs wickets strike_rate
player
Virat Kohli RCB Batter 750 0 154.7
Rohit Sharma MI Batter 417 0 150.0How it works: at/iat assignment changes the cell in the original DataFrame in one step — it's a single indexing call, so it is safe under Copy-on-Write. If you at-assign to a row label that doesn't exist, pandas adds a new row (filling other columns with NaN, which turns int columns into float) — handy, but easy to do by accident with a typo.
Example — what at/iat refuse
python
p.at["MS Dhoni", ["runs", "wickets"]] # InvalidIndexError: ['runs', 'wickets']
p.at["Virat Kohli":"MS Dhoni", "runs"] # InvalidIndexError: slice('Virat Kohli', 'MS Dhoni', None)
p.at[0, "runs"] # KeyError: 0 (0 is not a label here)
p.iat[0, "runs"] # ValueError: iAt based indexing can only have integer indexersHow it works: lists and slices are rejected because at promises a single value. at needs labels (and 0 isn't one after set_index), and iat needs integers only.
Comparison — choosing the right accessor
| You want… | By label | By position |
|---|---|---|
| One cell (read/write) | df.at["MS Dhoni", "runs"] | df.iat[2, 3] |
| One row | df.loc["MS Dhoni"] | df.iloc[2] |
| A block of rows/columns | df.loc["A":"B", ["x", "y"]] | df.iloc[0:3, 1:4] |
| Rows matching a condition | df.loc[df["runs"] > 400] | — |
loc[r, c] and at[r, c] return the same value; at simply states your intent ("exactly one cell") and skips extra checks. Under the hood pandas is so heavily optimised that in everyday code the difference is tiny — use at/iat mainly inside loops, and loc/iloc everywhere else.
Common mistakes
- ❌
df.at[0:3, "runs"]ordf.at[0, ["a", "b"]]. ✅atis single-cell only — uselocfor slices/lists. - ❌
df.iat[0, "runs"]. ✅iatneeds positions:df.iat[0, df.columns.get_loc("runs")], or usedf.at[0, "runs"]. - ❌ A typo like
df.at["MS Doni", "runs"] = 200silently adds a new row. ✅ Checkdf.index/ use"MS Doni" in df.indexbefore writing. - ❌ Writing a loop with
atto update a whole column. ✅ Vectorise:df["runs"] = df["runs"] + 10ordf.loc[mask, "runs"] = ...is far faster than any loop.
Interview tip
Q: What is the difference between at and loc?
A: Both are label-based, but at accesses exactly one scalar (one row label + one column label), while loc can return rows, columns, slices and boolean-filtered sets. Because at handles only the scalar case it has less overhead, which helps when setting or getting many individual cells in a loop.
Key points to remember
- .at[row_label, col_label] reads/writes one cell by label; .iat[i, j] does it by integer position.
- at pairs with loc (labels) and iat pairs with iloc (positions).
- at/iat accept only single keys — lists, slices and masks raise errors.
- They have less overhead than loc/iloc, which matters mostly inside loops.
- df.at[label, col] = value updates the original DataFrame in one step (safe under Copy-on-Write).
- Assigning with at to a missing row label adds a new row — watch for typos.
Try it yourself
Read single cells with at and iat
Update a cell and see the errors
Practice: update the scorecard
Revision Flashcards
Tap a card to flip it and see the answer.
