Use sort_values() to order records by scores, amounts, dates or other fields. Choose tie-breaking keys, missing-value placement and index handling so the report order is deliberate and repeatable.
sort_values() normally reorders rows using values in one or more columns. It does not independently rearrange each column: the fields in a record stay together. The default is ascending=True and missing values last. by selects the sort fields; axis selects the axis; ascending sets directions; na_position places missing values; ignore_index creates new consecutive labels; key supplies a vectorised sorting transformation. Use sort_index() when you want label order.
Use kind="stable" for a single-key sort when tied marks should retain their input order. The original table remains unchanged unless you request inplace=True. The displayed index labels still identify the source rows.
import pandas as pd
def student_data():
return pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex", "Ron", "King", "Jack"], "ID": [1, 2, 3, 4, 5, 6], "MATH": [80, 40, 70, 70, 70, 30], "ENGLISH": [80, 70, 40, 50, 60, 30]})
df = student_data()
result = df.sort_values("MATH", kind="stable")
print(result)
assert result["ID"].tolist() == [6, 2, 3, 4, 5, 1]
assert df["ID"].tolist() == [1, 2, 3, 4, 5, 6]Expected output
NAME ID MATH ENGLISH
5 Jack 6 30 30
1 Raju 2 40 70
2 Alex 3 70 40
3 Ron 4 70 50
4 King 5 70 60
0 Ravi 1 80 80Sort first by MATH, then ENGLISH within tied MATH values. A final unique ID key can make the required tie-breaking order explicit for repeatable reports.
df = student_data()
result = df.sort_values(["MATH", "ENGLISH", "ID"])
print(result)
assert result["ID"].tolist() == [6, 2, 3, 4, 5, 1]Expected output
NAME ID MATH ENGLISH
5 Jack 6 30 30
1 Raju 2 40 70
2 Alex 3 70 40
3 Ron 4 70 50
4 King 5 70 60
0 Ravi 1 80 80ascending=False reverses the value ordering, not the source table’s index labels. With a single-key stable sort, tied marks retain their source order.
df = student_data()
result = df.sort_values("MATH", ascending=False, kind="stable")
print(result)
assert result["ID"].tolist() == [1, 3, 4, 5, 2, 6]Expected output
NAME ID MATH ENGLISH
0 Ravi 1 80 80
2 Alex 3 70 40
3 Ron 4 70 50
4 King 5 70 60
1 Raju 2 40 70
5 Jack 6 30 30An ascending list must match the number of by fields. This report puts MATH highest first, then ENGLISH lowest first and ID lowest first.
df = student_data()
result = df.sort_values(["MATH", "ENGLISH", "ID"], ascending=[False, True, True])
print(result)
assert result["ID"].tolist() == [1, 3, 4, 5, 2, 6]Expected output
NAME ID MATH ENGLISH
0 Ravi 1 80 80
2 Alex 3 70 40
3 Ron 4 70 50
4 King 5 70 60
1 Raju 2 40 70
5 Jack 6 30 30The student names are sorted in descending text order. For mixed case, key=lambda column: column.str.casefold() creates a case-insensitive comparison key without changing the stored names. This is not a locale-specific collation rule.
df = student_data()
print(df.sort_values("NAME", ascending=False))
names = pd.DataFrame({"name": ["bob", "Alice", "alice", "Carol"]})
result = names.sort_values("name", key=lambda column: column.str.casefold(), kind="stable")
print("Case-insensitive:")
print(result)
assert result["name"].tolist() == ["Alice", "alice", "bob", "Carol"]Expected output
NAME ID MATH ENGLISH
3 Ron 4 70 50
0 Ravi 1 80 80
1 Raju 2 40 70
4 King 5 70 60
5 Jack 6 30 30
2 Alex 3 70 40
Case-insensitive:
name
1 Alice
2 alice
0 bob
3 Carolna_position="first" places missing values first regardless of ascending. Sorting does not validate or fill values. Keep missing-score records visible for review when that is the report’s purpose.
df = student_data()
df["MATH"] = df["MATH"].astype("Int64")
df.loc[1, "MATH"] = pd.NA
result = df.sort_values("MATH", ascending=False, na_position="first", kind="stable")
print(result)
assert result.iloc[0]["ID"] == 2
assert result["MATH"].isna().sum() == 1Expected output
NAME ID MATH ENGLISH
1 Raju 2 <NA> 70
0 Ravi 1 80 80
2 Alex 3 70 40
3 Ron 4 70 50
4 King 5 70 60
5 Jack 6 30 30ignore_index=True assigns labels 0 through n-1 after sorting. It does not change an ID column. Use loc for labels and iloc for positions; the first displayed row need not have label 0 unless you reset it.
df = student_data()
result = df.sort_values("MATH", ignore_index=True, kind="stable")
print(result)
assert result.index.tolist() == list(range(6))
assert result.loc[0, "ID"] == 6
assert result.iloc[0]["NAME"] == "Jack"Expected output
NAME ID MATH ENGLISH
0 Jack 6 30 30
1 Raju 2 40 70
2 Alex 3 70 40
3 Ron 4 70 50
4 King 5 70 60
5 Ravi 1 80 80Text numbers sort lexicographically rather than numerically. Parse and review failures before using a numeric sort. Parse dates with a known source format rather than relying on how the strings look.
raw = pd.DataFrame({"id": [1, 2, 3, 4], "amount": ["100", "20", "3", "bad"], "date": ["02/09/2026", "01/10/2026", "01/09/2026", "bad"]})
cleaned = raw.copy()
cleaned["amount"] = pd.to_numeric(raw["amount"], errors="coerce")
cleaned["date"] = pd.to_datetime(raw["date"], format="%d/%m/%Y", errors="coerce")
print("Text amount order:", raw.sort_values("amount")["id"].tolist())
print("Numeric amount order:")
print(cleaned.sort_values(["amount", "id"], na_position="last"))
print("Date order:", cleaned.sort_values(["date", "id"])["id"].tolist())
assert cleaned.sort_values(["amount", "id"])["id"].tolist() == [3, 2, 1, 4]
assert cleaned.sort_values(["date", "id"])["id"].tolist() == [3, 1, 2, 4]Expected output
Text amount order: [1, 2, 3, 4]
Numeric amount order:
id amount date
2 3 3.0 2026-09-01
1 2 20.0 2026-10-01
0 1 100.0 2026-09-02
3 4 NaN NaT
Date order: [3, 1, 2, 4]Alphabetical order may not match a reporting rule. Validate labels before converting them to a fixed categorical dtype; unexpected labels can otherwise become missing. This example puts High before Medium before Low.
tasks = pd.DataFrame({"id": [1, 2, 3, 4], "priority": ["Low", "High", "Medium", "High"]})
order = ["High", "Medium", "Low"]
assert tasks["priority"].isin(order).all()
tasks["priority"] = tasks["priority"].astype(pd.CategoricalDtype(order, ordered=True))
result = tasks.sort_values(["priority", "id"])
print(result)
assert result["id"].tolist() == [2, 4, 3, 1]Expected output
id priority
1 2 High
3 4 High
2 3 Medium
0 1 Lowinplace=True updates the DataFrame itself. Explicitly assigning a returned sorted table is often easier to follow, particularly when retaining a raw copy. Do not rely on chained edits to a selected column.
df = student_data()
df.sort_values(["MATH", "ID"], inplace=True)
print(df)
assert df["ID"].tolist() == [6, 2, 3, 4, 5, 1]Expected output
NAME ID MATH ENGLISH
5 Jack 6 30 30
1 Raju 2 40 70
2 Alex 3 70 40
3 Ron 4 70 50
4 King 5 70 60
0 Ravi 1 80 80axis=1 sorts columns using row labels specified in by. It is a different operation from ordering rows by a column. For alphabetical column labels, use sort_index(axis=1).
scores = pd.DataFrame({"MATH": [70, 80], "ENGLISH": [50, 90], "SCIENCE": [60, 75]}, index=["Alex", "Ravi"])
result = scores.sort_values(by="Alex", axis=1)
print(result)
assert result.columns.tolist() == ["ENGLISH", "SCIENCE", "MATH"]Expected output
ENGLISH SCIENCE MATH
Alex 50 60 70
Ravi 90 75 80This synthetic extract has tied amounts, a test order and an invalid amount. Define the report population first, then sort by amount descending, date ascending and order ID ascending. Retain excluded raw records for review.
raw = pd.DataFrame({"order_id": [101, 102, 103, 104, 105, 106], "date": ["2026-09-02", "2026-09-01", "2026-09-03", "2026-09-04", "2026-09-01", "2026-09-01"], "status": ["paid", "paid", "test", "paid", "paid", "paid"], "amount": ["20", "20", "50", "bad", "10", "20"]})
cleaned = raw.copy()
cleaned["date"] = pd.to_datetime(raw["date"], format="%Y-%m-%d", errors="coerce")
cleaned["amount"] = pd.to_numeric(raw["amount"], errors="coerce")
valid = cleaned["status"].eq("paid") & cleaned["date"].notna() & cleaned["amount"].ge(0).fillna(False)
accepted = cleaned.loc[valid].copy()
report = accepted.sort_values(["amount", "date", "order_id"], ascending=[False, True, True], ignore_index=True)
review = raw.loc[~valid].copy()
print("Sorted report:")
print(report)
print("Excluded records:")
print(review)
assert report["order_id"].tolist() == [102, 106, 101, 105]
assert report["amount"].sum() == accepted["amount"].sum() == 70
assert review["order_id"].tolist() == [103, 104]Expected output
Sorted report:
order_id date status amount
0 102 2026-09-01 paid 20.0
1 106 2026-09-01 paid 20.0
2 101 2026-09-02 paid 20.0
3 105 2026-09-01 paid 10.0
Excluded records:
order_id date status amount
2 103 2026-09-03 test 50
3 104 2026-09-04 paid badhead(3) returns exactly three rows after sorting, so the explicit tie-breaking rule determines which rows appear. If all tied top values should be included, define that rule separately; see nlargest(). Sorting alone should preserve record membership and totals.
top = report.head(3)
print("Top three:")
print(top)
print(report.to_csv(index=False).rstrip())
assert set(report["order_id"]) == set(accepted["order_id"])
assert top["amount"].sum() == 60
# Optional Colab export:
# report.to_csv("sorted_paid_orders.csv", index=False)Expected output
Top three:
order_id date status amount
0 102 2026-09-01 paid 20.0
1 106 2026-09-01 paid 20.0
2 101 2026-09-02 paid 20.0
order_id,date,status,amount
102,2026-09-01,paid,20.0
106,2026-09-01,paid,20.0
101,2026-09-02,paid,20.0
105,2026-09-01,paid,10.0Why is 100 before 20? Check whether the values are strings. Why did index labels stay out of order? Sorting preserves row labels unless ignore_index=True. Why did tied records change order? Specify a stable single-key sort or explicit secondary keys. kind only applies to a single-column/label DataFrame sort. Does sorting remove duplicates? No; use a separate validated rule. Does sorting fill missing values? No; na_position only chooses their placement. Why did a key function fail? It must accept a Series and return a same-shaped result; for mixed field types, prepare dedicated sort keys. Continue with filtering rows, group summaries and plotting.
Use IDs 3, 1 and 2 with scores 80, 80 and 70. Sort score descending and ID ascending, with a fresh consecutive index. Predict the ID order.
ID breaks the tie between the two scores of 80.
practice = pd.DataFrame({"ID": [3, 1, 2], "score": [80, 80, 70]})
answer = practice.sort_values(["score", "ID"], ascending=[False, True], ignore_index=True)
print(answer)
assert answer["ID"].tolist() == [1, 3, 2]
assert answer.index.tolist() == [0, 1, 2]Expected output
ID score
0 1 80
1 3 80
2 2 70Open in Google Colab View on GitHub
Run the examples in order, change sort directions and tied values, then inspect record order and preserved totals. Save your own copy to keep edits. All sample tables are included in the notebook.
Continue with the Data Cleaning hub, where(), loc, at, NumPy where(), fillna() and iloc.
Reference: Pandas DataFrame.sort_values 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.