Use at with square brackets to read or update one cell by its row and column labels. Validate labels before changing a business record.
at is an indexer, not a function: use square brackets. The first label identifies a row and the second identifies a column. This small embedded table makes the examples runnable without uploading a workbook.
import pandas as pd
from io import BytesIO
def student_data():
return pd.DataFrame({
"id": [1, 2, 3, 4],
"name": ["John Deo", "Max Ruin", "Arnold", "Krish Star"],
"class": ["Four", "Three", "Three", "Four"],
"mark": [75, 85, 55, 60],
"sex": ["female", "male", "male", "female"]
})
df = student_data()
print(df)Expected output
id name class mark sex
0 1 John Deo Four 75 female
1 2 Max Ruin Three 85 male
2 3 Arnold Three 55 male
3 4 Krish Star Four 60 femaleWith the default index, row label 1 happens to identify the second row. That relationship changes when labels are replaced or rows are reordered.
df = student_data()
print(df.at[1, "name"])
assert df.at[1, "name"] == "Max Ruin"Expected output
Max RuinA row selected with loc is a Series, so its at indexer accepts one label. For a direct cell lookup, use DataFrame.at with both labels. Use the direct form for updates to the original DataFrame.
df = student_data()
print(df.loc[2].at["name"])
print(df.at[2, "name"])
assert df.loc[2].at["name"] == df.at[2, "name"] == "Arnold"Expected output
Arnold
ArnoldAssign through the DataFrame indexer. Avoid chained selection when changing the table. Start each student example from a fresh source to make its result independent.
df = student_data()
df.at[2, "name"] = "Arnold2"
print(df.at[2, "name"])
print(df)
assert df.at[2, "name"] == "Arnold2"Expected output
Arnold2
id name class mark sex
0 1 John Deo Four 75 female
1 2 Max Ruin Three 85 male
2 3 Arnold2 Three 55 male
3 4 Krish Star Four 60 femaleCreate row labels with set_index(). Label 2 now identifies Max Ruin. iat uses integer positions; it is also an indexer and takes square brackets.
indexed = student_data().set_index("id")
print("Label 2:", indexed.at[2, "name"])
print("Position 2:", indexed.iat[2, 0])
assert indexed.at[2, "name"] == "Max Ruin"
assert indexed.iat[2, 0] == "Arnold"Expected output
Label 2: Max Ruin
Position 2: Arnoldloc selects labels, lists and masks. Use at for a single label pair on a table with unique row and column labels. iloc selects by position.
indexed = student_data().set_index("id")
print("One cell:", indexed.at[3, "mark"])
print("A small table:")
print(indexed.loc[[2, 3], ["name", "mark"]])
assert indexed.at[3, "mark"] == indexed.loc[3, "mark"] == 55Expected output
One cell: 55
A small table:
name mark
id
2 Max Ruin 85
3 Arnold 55Reading a nonexistent row or column label raises KeyError. Check the labels or handle the error according to the application rule.
indexed = student_data().set_index("id")
try:
print(indexed.at[99, "name"])
except KeyError:
print("Student ID 99 does not exist")
print("Known IDs:", indexed.index.tolist())Expected output
Student ID 99 does not exist
Known IDs: [1, 2, 3, 4]Assignment to a missing label can enlarge the DataFrame. For an update-only operation, validate uniqueness and both labels before assigning. Blank or missing business keys need separate validation.
def update_cell(frame, row_label, column_label, value):
if not frame.index.is_unique or not frame.columns.is_unique:
raise ValueError("Cell updates require unique row and column labels")
if row_label not in frame.index or column_label not in frame.columns:
raise KeyError("Unknown row or column label")
frame.at[row_label, column_label] = value
indexed = student_data().set_index("id")
try:
update_cell(indexed, 99, "mark", 70)
except KeyError:
print("Unknown update rejected")
update_cell(indexed, 3, "mark", 58)
print(indexed.loc[[3]])
assert len(indexed) == 4 and indexed.at[3, "mark"] == 58Expected output
Unknown update rejected
name class mark sex
id
3 Arnold Three 58 maleDuplicate labels make a supposedly single-cell selection ambiguous. Inspect all conflicting rows with duplicated() and loc rather than assuming one scalar result.
df = student_data()
df.loc[3, "id"] = 3
indexed = df.set_index("id")
print("Unique row labels:", indexed.index.is_unique)
print("Conflicting ID 3 rows:")
print(indexed.loc[[3]])
try:
update_cell(indexed, 3, "mark", 80)
except ValueError as error:
print(error)
assert not indexed.index.is_uniqueExpected output
Unique row labels: False
Conflicting ID 3 rows:
name class mark sex
id
3 Arnold Three 55 male
3 Krish Star Four 60 female
Cell updates require unique row and column labelsA compatible nullable dtype allows numeric values and missing markers in the same column. Replacing a value must respect the column dtype. Do not force unrelated text into a numeric measure.
df = student_data().set_index("id")
df["mark"] = df["mark"].astype("Int64")
df.at[3, "mark"] = pd.NA
print("Missing:", pd.isna(df.at[3, "mark"]))
df.at[3, "mark"] = 58
print("Updated mark:", df.at[3, "mark"])
assert df["mark"].dtype == "Int64"Expected output
Missing: True
Updated mark: 58For your workbook, pass its path to read_excel(). Here an in-memory workbook demonstrates the same process. Excel needs openpyxl; the rest of the notebook can continue if it is missing. See read_excel() and sample student files.
try:
import openpyxl
except ImportError:
print("Excel example requires openpyxl; continue with embedded tables.")
else:
workbook = BytesIO()
student_data().to_excel(workbook, index=False, engine="openpyxl")
workbook.seek(0)
imported = pd.read_excel(workbook, engine="openpyxl")
print(imported.at[1, "name"])
assert imported.at[1, "name"] == "Max Ruin"Expected output
Max RuinThis synthetic example has a known correction: order 102 should have quantity 3. Validate the ID and value, keep a source copy and record the change. This demonstrates the mechanics; real corrections need evidence from the source record.
raw = pd.DataFrame({
"order_id": [101, 102, 103],
"product": ["Pen", "Book", "Pencil"],
"quantity": pd.Series([2, 1, 4], dtype="Int64"),
"unit_price": [5, 20, 2]
})
orders = raw.set_index("order_id").copy()
order_id, corrected_quantity = 102, 3
if not isinstance(corrected_quantity, int) or corrected_quantity < 1:
raise ValueError("Quantity must be a positive integer")
old_quantity = orders.at[order_id, "quantity"]
update_cell(orders, order_id, "quantity", corrected_quantity)
audit = pd.DataFrame({"order_id": [order_id], "field": ["quantity"],
"old_value": [old_quantity], "new_value": [corrected_quantity]})
print(orders)
print("Change record:")
print(audit)
assert raw.loc[raw["order_id"].eq(102), "quantity"].iloc[0] == 1
assert orders.at[102, "quantity"] == 3Expected output
product quantity unit_price
order_id
101 Pen 2 5
102 Book 3 20
103 Pencil 4 2
Change record:
order_id field old_value new_value
0 102 quantity 1 3Changing quantity does not automatically update a previously calculated revenue column. Recalculate from the current values and check the affected record. Use vectorized operations for whole columns rather than a cell-by-cell loop.
orders["revenue"] = orders["quantity"] * orders["unit_price"]
print(orders)
print("Corrected order revenue:", orders.at[102, "revenue"])
print("Total revenue:", orders["revenue"].sum())
assert orders.at[102, "revenue"] == 60
assert orders["revenue"].sum() == 78Expected output
product quantity unit_price revenue
order_id
101 Pen 2 5 10
102 Book 3 20 60
103 Pencil 4 2 8
Corrected order revenue: 60
Total revenue: 78Use reset_index() to restore order_id as a column. Export that table with index=False to retain the business key without an extra row-number column. See Input and Output.
export_table = orders.reset_index()
print(export_table.to_csv(index=False).rstrip())
assert export_table["order_id"].tolist() == [101, 102, 103]
# Optional Colab exports:
# export_table.to_csv("corrected_orders.csv", index=False)
# audit.to_csv("order_change_record.csv", index=False)Expected output
order_id,product,quantity,unit_price,revenue
101,Pen,2,5,10
102,Book,3,20,60
103,Pencil,4,2,8Is at a function? No; use square brackets. Does an integer always mean a row position? No; at treats it as a label. Can at select a list? Use loc for lists, masks and ranges. Can an update create a row? Assignment can enlarge the table; validate labels for update-only operations. Will editing a selected Series update the source? Assign directly through the DataFrame. Is at always faster? Choose it for scalar access and measure important workloads. Continue with mask(), where(), query() and Data Cleaning.
Set id as the index of a fresh student table, change ID 4 to mark 65 and confirm that the original source table still has mark 60 for that student.
Update the copy through its label pair. ID 4 is Krish Star.
source = student_data()
answer = source.set_index("id").copy()
update_cell(answer, 4, "mark", 65)
print(answer.loc[[4]])
assert answer.at[4, "mark"] == 65
assert source.loc[source["id"].eq(4), "mark"].iloc[0] == 60Expected output
name class mark sex
id
4 Krish Star Four 65 femaleOpen in Google Colab View on GitHub
Run the examples in order, change IDs and corrected quantities, then inspect the affected cell and recalculated totals. Save your own copy to keep edits. All sample tables are included in the notebook.
Reference: Pandas DataFrame.at documentation.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.