Use loc with square brackets to select and update rows and columns by labels or Boolean conditions. Keep labels distinct from row positions.
Watch: selecting rows with Pandas loc
The embedded student sample uses the same columns as the downloadable CSV. For your file replace StringIO(student_csv) with a path or URL, or use read_excel() for a workbook. See read_csv().
import pandas as pd
import re
from io import StringIO
student_csv = 'id,name,class,mark,gender\n1,John Deo,Four,75,female\n2,Max Ruin,Three,85,male\n3,Arnold,Three,55,male\n4,Krish Star,Four,60,female\n5,John Mike,Four,60,female\n6,Alex John,Four,55,male\n7,My John Rob,Fifth,78,male\n8,Asruid,Five,85,male\n9,Tes Qry,Six,78,male\n10,Big John,Four,55,female\n11,Ronald,Six,89,female\n12,Recky,Six,94,female\n13,Kty,Seven,88,female\n14,Bigy,Seven,88,female\n15,Tade Row,Four,88,male\n16,Gimmy,Four,88,male\n17,Tumyu,Six,54,male\n18,Honny,Five,75,male\n19,Tinny,Nine,18,male\n20,Jackly,Nine,65,female\n21,Babby John,Four,69,female\n22,Reggid,Seven,55,female\n23,Herod,Eight,79,male\n24,Tiddy Now,Seven,78,male\n25,Giff Tow,Seven,88,male\n26,Crelea,Seven,79,male\n27,Big Nose,Three,81,female\n28,Rojj Base,Seven,86,female\n29,Tess Played,Seven,55,male\n30,Reppy Red,Six,79,female\n31,Marry Toeey,Four,88,male\n32,Binn Rott,Seven,90,female\n33,Kenn Rein,Six,96,female\n34,Gain Toe,Seven,69,male\n35,Rows Noump,Six,88,female'
def student_data():
return pd.read_csv(StringIO(student_csv))
df = student_data()
print(df.head())
print("Shape:", df.shape)Expected output
id name class mark gender
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 female
4 5 John Mike Four 60 female
Shape: (35, 5)loc is an indexer, not a function. Use df.loc[row_selector, column_selector]. Integers identify index labels; iloc uses integer positions. A colon selects all labels on an axis. Boolean Series align by index labels.
A single column label returns a Series; a list of column labels returns a DataFrame.
df = student_data()
print(df.loc[:, "class"].head())
print(df.loc[:, ["class", "name"]].head())
assert df.loc[:, ["class", "name"]].shape == (35, 2)Expected output
0 Four
1 Three
2 Three
3 Four
4 Four
Name: class, dtype: str
class name
0 Four John Deo
1 Three Max Ruin
2 Three Arnold
3 Four Krish Star
4 Four John MikeWith the default index, label 2 is the third row. Use a list to retain a DataFrame. Use at for a single cell on uniquely labelled axes.
df = student_data()
print(df.loc[0, ["class", "name"]])
print(df.loc[[2, 3], ["class", "name"]])
print(df.loc[[2, 3], :])
print("Arnold mark:", df.loc[2, "mark"])
assert df.loc[2, "name"] == "Arnold"Expected output
class Four
name John Deo
Name: 0, dtype: str
class name
2 Three Arnold
3 Four Krish Star
id name class mark gender
2 3 Arnold Three 55 male
3 4 Krish Star Four 60 female
Arnold mark: 55Set name as the index to select name labels. A list selection follows the requested label order. Missing requested labels raise KeyError.
named = student_data().set_index("name")
print(named.loc["Tade Row"])
print(named.loc[["Tade Row", "John Deo"]])
print(named.loc["Arnold", "mark":])
print(named.loc[:, "mark"].head())
print(named.loc["Arnold", :])
assert named.loc["Tade Row", "id"] == 15Expected output
id 15
class Four
mark 88
gender male
Name: Tade Row, dtype: object
id class mark gender
name
Tade Row 15 Four 88 male
John Deo 1 Four 75 female
mark 55
gender male
Name: Arnold, dtype: object
name
John Deo 75
Max Ruin 85
Arnold 55
Krish Star 60
John Mike 60
Name: mark, dtype: int64
id 3
class Three
mark 55
gender male
Name: Arnold, dtype: objectA class label can identify many students. Duplicate labels are valid for grouped selection, but cannot identify one unique business record without another key.
by_class = student_data().set_index("class")
print(by_class.loc["Four"])
assert not by_class.index.is_uniqueExpected output
id name mark gender
class
Four 1 John Deo 75 female
Four 4 Krish Star 60 female
Four 5 John Mike 60 female
Four 6 Alex John 55 male
Four 10 Big John 55 female
Four 15 Tade Row 88 male
Four 16 Gimmy 88 male
Four 21 Babby John 69 female
Four 31 Marry Toeey 88 maleLabel slices include both endpoints. An unsorted unique index with existing bounds follows its current order, not alphabetical order. Nonunique unsorted bounds can be ambiguous; for value ranges use a mask or sort the index under an explicit rule.
by_id = student_data().set_index("id").sort_index()
print(by_id.loc[4:6])
named = student_data().set_index("name")
print(named.loc["Ronald":"Bigy"])
assert by_id.loc[4:6].index.tolist() == [4, 5, 6]
assert named.loc["Ronald":"Bigy"].index.tolist() == ["Ronald", "Recky", "Kty", "Bigy"]Expected output
name class mark gender
id
4 Krish Star Four 60 female
5 John Mike Four 60 female
6 Alex John Four 55 male
id class mark gender
name
Ronald 11 Six 89 female
Recky 12 Six 94 female
Kty 13 Seven 88 female
Bigy 14 Seven 88 femaleA callable receives the current table. Reference a name column before moving it to the index; afterwards use the index explicitly. Boolean results must have compatible labels or the correct array length.
df = student_data()
print(df.loc[lambda x: x["name"].eq("Arnold")])
print(df.loc[lambda x: x["mark"].gt(90)])
print(df.loc[lambda x: x["gender"].eq("male")].head())
print(df.loc[lambda x: x["name"].str.len().gt(10)].head())
named = df.set_index("name")
print(named.loc[lambda x: x.index.str.len() > 10].head())Expected output
id name class mark gender
2 3 Arnold Three 55 male
id name class mark gender
11 12 Recky Six 94 female
32 33 Kenn Rein Six 96 female
id name class mark gender
1 2 Max Ruin Three 85 male
2 3 Arnold Three 55 male
5 6 Alex John Four 55 male
6 7 My John Rob Fifth 78 male
7 8 Asruid Five 85 male
id name class mark gender
6 7 My John Rob Fifth 78 male
28 29 Tess Played Seven 55 male
30 31 Marry Toeey Four 88 male
id class mark gender
name
My John Rob 7 Fifth 78 male
Tess Played 29 Seven 55 male
Marry Toeey 31 Four 88 maleUse eq() for exact values. str.contains() can match partial text. Literal matching with na=False makes missing names nonmatches.
df = student_data()
print(df.loc[df["name"].eq("Alex John")])
print(df.loc[df["name"].str.contains("De", case=False, regex=False, na=False)])
assert df.loc[df["name"].eq("Alex John"), "id"].tolist() == [6]Expected output
id name class mark gender
5 6 Alex John Four 55 male
id name class mark gender
0 1 John Deo Four 75 female
14 15 Tade Row Four 88 maleSpecify missing-value treatment before inversion. With na=False, inversion includes missing names; add notna() when only known names should be kept.
df = student_data()
ends = df["name"].str.endswith("John", na=False)
print(df.loc[ends])
print("Not ending with John IDs:", df.loc[~ends, "id"].tolist())
assert df.loc[ends, "id"].tolist() == [6, 10, 21]Expected output
id name class mark gender
5 6 Alex John Four 55 male
9 10 Big John Four 55 female
20 21 Babby John Four 69 female
Not ending with John IDs: [1, 2, 3, 4, 5, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]startswith() is a literal prefix check. Case sensitivity applies to the text as stored.
df = student_data()
starts = df["name"].str.startswith("Jo", na=False)
print(df.loc[starts])
print("Other IDs:", df.loc[~starts, "id"].tolist())
assert df.loc[starts, "id"].tolist() == [1, 5]Expected output
id name class mark gender
0 1 John Deo Four 75 female
4 5 John Mike Four 60 female
Other IDs: [2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]Regex alternation matches either term. Escape terms when constructing a regex from literal strings, or combine separate literal masks with |.
df = student_data()
terms = ["Jo", "Al"]
pattern = "|".join(re.escape(term) for term in terms)
any_term = df["name"].str.contains(pattern, na=False)
print(df.loc[any_term])
print("Neither term IDs:", df.loc[~any_term, "id"].tolist())
print("No Jo IDs:", df.loc[~df["name"].str.contains("Jo", regex=False, na=False), "id"].tolist())Expected output
id name class mark gender
0 1 John Deo Four 75 female
4 5 John Mike Four 60 female
5 6 Alex John Four 55 male
6 7 My John Rob Fifth 78 male
9 10 Big John Four 55 female
20 21 Babby John Four 69 female
Neither term IDs: [2, 3, 4, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]
No Jo IDs: [2, 3, 4, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]Use & for AND and | for OR; Python and/or do not combine whole Series. Both masks must describe the same table.
df = student_data()
jo = df["name"].str.contains("Jo", regex=False, na=False)
al = df["name"].str.contains("Al", regex=False, na=False)
print("Both:")
print(df.loc[jo & al])
print("Either IDs:", df.loc[jo | al, "id"].tolist())
assert df.loc[jo & al, "id"].tolist() == [6]Expected output
Both:
id name class mark gender
5 6 Alex John Four 55 male
Either IDs: [1, 5, 6, 7, 10, 21]Parentheses separate comparisons. Build named masks when the rule becomes hard to read. See query() for fixed expression-based filters.
df = student_data()
print(df.loc[df["name"].str.contains("Mike", regex=False, na=False) | df["id"].eq(6)])
by_id = df["id"].eq(5)
by_name = df["name"].str.contains("Al", case=False, regex=False, na=False)
low_mark = df["mark"].lt(50)
print(df.loc[by_id | by_name | low_mark])Expected output
id name class mark gender
4 5 John Mike Four 60 female
5 6 Alex John Four 55 male
id name class mark gender
4 5 John Mike Four 60 female
5 6 Alex John Four 55 male
10 11 Ronald Six 89 female
18 19 Tinny Nine 18 maleloc aligns a Boolean Series to the row index. A Boolean list or array is positional and must match the row count. Build masks from the current table to avoid unintended alignment.
small = pd.DataFrame({"amount": [10, 20, 30]}, index=[101, 102, 103])
mask = pd.Series([True, False, True], index=[103, 101, 102])
print(small.loc[mask])
print("Array selection:")
print(small.loc[mask.to_numpy()])
assert small.loc[mask].index.tolist() == [102, 103]Expected output
amount
102 20
103 30
Array selection:
amount
101 10
103 30Use tail() for final positions. A negative loc label is a label lookup, not a shortcut for the last row.
df = student_data()
print(df.tail(1))
print(df.tail(5))
assert df.tail(1)["id"].tolist() == [35]Expected output
id name class mark gender
34 35 Rows Noump Six 88 female
id name class mark gender
30 31 Marry Toeey Four 88 male
31 32 Binn Rott Seven 90 female
32 33 Kenn Rein Six 96 female
33 34 Gain Toe Seven 69 male
34 35 Rows Noump Six 88 femaledf.loc[len(df)] is appropriate only when that label is unused, as with this fresh default index. On a filtered or differently indexed table it can overwrite an existing label. Validate the label and match values to the column order.
df = student_data()
new_label = len(df)
if new_label in df.index:
raise ValueError("New label already exists")
new_list = [36, "new name", "Four", 77, "male"]
df.loc[new_label] = new_list
print(df.tail(2))
assert len(df) == 36 and df.loc[new_label, "id"] == 36Expected output
id name class mark gender
34 35 Rows Noump Six 88 female
35 36 new name Four 77 maleAssign with one loc operation. Each example below uses a fresh table. In this demonstration the rule sets high marks to 100; a real grade change requires an approved rule.
df = student_data()
df.loc[df["mark"].ge(90), "mark"] = 100
print(df.loc[df["mark"].eq(100)])
df = student_data()
condition = df["mark"].ge(90) & df["class"].eq("Six")
df.loc[condition, "mark"] = 100
print("Class Six updates:")
print(df.loc[condition])Expected output
id name class mark gender
11 12 Recky Six 100 female
31 32 Binn Rott Seven 100 female
32 33 Kenn Rein Six 100 female
Class Six updates:
id name class mark gender
11 12 Recky Six 100 female
32 33 Kenn Rein Six 100 femaleChanging the in-memory DataFrame does not write back to the original CSV. Keep source values or a change record when corrections must be traceable.
df = student_data()
condition = df["mark"].ge(90) & df["class"].eq("Six")
df.loc[condition, "class"] = "Seven"
print(df.loc[condition])
df = student_data()
df.loc[df["name"].eq("Rows Noump"), "name"] = "New Name"
print(df.tail(1))
assert df.loc[34, "name"] == "New Name"Expected output
id name class mark gender
11 12 Recky Seven 94 female
32 33 Kenn Rein Seven 96 female
id name class mark gender
34 35 New Name Six 88 femaleThis synthetic table preserves invalid amounts for review. Clean numeric values before combining selection rules. The Data Cleaning hub covers related validation steps.
raw = pd.DataFrame({
"order_id": [101, 102, 103, 104, 105],
"status": ["paid", "paid", "test", "paid", "paid"],
"amount": ["20", "35", "99", "bad", "10"]
})
amount = pd.to_numeric(raw["amount"], errors="coerce")
review = raw.loc[amount.isna()].copy()
eligible = raw["status"].eq("paid") & amount.ge(20)
selected = raw.loc[eligible, ["order_id", "amount"]].copy()
selected["amount"] = amount.loc[selected.index]
print(selected)
print("Review IDs:", review["order_id"].tolist())
assert selected["order_id"].tolist() == [101, 102]
assert selected["amount"].sum() == 55Expected output
order_id amount
0 101 20.0
1 102 35.0
Review IDs: [104]Set flags on the working table with one loc assignment. Export selected orders separately from the review data; unknown amounts should not become zero merely to pass a numeric rule. See Input and Output.
working = raw.copy()
working["needs_review"] = False
working.loc[amount.isna(), "needs_review"] = True
print(working)
print(selected.to_csv(index=False).rstrip())
assert working["needs_review"].sum() == 1
# Optional Colab exports:
# selected.to_csv("selected_orders.csv", index=False)
# review.to_csv("review_orders.csv", index=False)Expected output
order_id status amount needs_review
0 101 paid 20 False
1 102 paid 35 False
2 103 test 99 False
3 104 paid bad True
4 105 paid 10 False
order_id,amount
101,20.0
102,35.0Why did loc[2] select a different row after indexing? 2 is a label. Are slice endpoints included? Yes, label slices include both bounds when resolvable. Why did selection return multiple rows? The row label may be duplicated. Why does a mask fail? Its index may be unalignable, or its array length may differ. Why did assignment replace a row? The requested label already existed. Should I edit a chained selection? Assign directly through the source DataFrame loc indexer. Continue with mask(), where(), NumPy where() and DataFrames.
From a fresh student table, select Class Four students with marks at least 80 and return only id, name and mark. Predict the result before running the solution.
Use one mask for both conditions and a column list to keep a DataFrame.
df = student_data()
mask = df["class"].eq("Four") & df["mark"].ge(80)
answer = df.loc[mask, ["id", "name", "mark"]]
print(answer)
assert answer["id"].tolist() == [15, 16, 31]Expected output
id name mark
14 15 Tade Row 88
15 16 Gimmy 88
30 31 Marry Toeey 88Open in Google Colab View on GitHub
Run the examples in order, change labels and filtering conditions, then inspect selections and independent updates. Save your own copy to keep edits. All sample tables are included in the notebook.
Reference: Pandas DataFrame.loc 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.