Use reset_index() to restore row labels as columns or discard labels that no longer matter. Preserve business identifiers and prepare grouped results for export.
Resetting an index changes row labels. It does not sort rows, remove duplicate records or create a permanent business identifier.
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()
print(df)Expected output
NAME ID MATH ENGLISH
0 Ravi 1 80 80
1 Raju 2 40 70
2 Alex 3 70 40
3 Ron 4 70 50
4 King 5 70 60
5 Jack 6 30 30level chooses which index levels to remove. drop=False preserves removed labels as columns; drop=True discards them. inplace=False returns a new table. names sets the inserted column names. col_level and col_fill control insertion into hierarchical columns. allow_duplicates=True permits duplicate column labels; resolving the naming conflict usually makes subsequent selection clearer.
Even an existing sequential index can become a column. Keep it only when those old labels have a purpose.
df = student_data()
reset = df.reset_index()
print(reset)
assert reset["index"].tolist() == list(range(6))
assert "index" not in df.columnsExpected output
index NAME ID MATH ENGLISH
0 0 Ravi 1 80 80
1 1 Raju 2 40 70
2 2 Alex 3 70 40
3 3 Ron 4 70 50
4 4 King 5 70 60
5 5 Jack 6 30 30Use set_index() to create the labels, then reset_index() to restore them as columns. The named index becomes a column with that name.
indexed = student_data().set_index("NAME")
restored = indexed.reset_index(drop=False)
print(restored)
assert restored["NAME"].tolist() == student_data()["NAME"].tolist()
assert restored.equals(student_data())Expected output
NAME ID MATH ENGLISH
0 Ravi 1 80 80
1 Raju 2 40 70
2 Alex 3 70 40
3 Ron 4 70 50
4 King 5 70 60
5 Jack 6 30 30drop=True is useful for row labels that no longer matter after filtering. It discards an identifier stored only in the index, so use it deliberately.
indexed = student_data().set_index("NAME")
discarded = indexed.reset_index(drop=True)
print(discarded)
assert "NAME" not in discarded.columns
assert discarded.index.tolist() == list(range(6))Expected output
ID MATH ENGLISH
0 1 80 80
1 2 40 70
2 3 70 40
3 4 70 50
4 5 70 60
5 6 30 30inplace=False leaves the original unchanged. A method call whose result is ignored does not reset the source.
indexed = student_data().set_index("ID")
indexed.reset_index(inplace=False)
print("Source index:", indexed.index.tolist())
result = indexed.reset_index(inplace=False)
print(result)
assert indexed.index.name == "ID"
assert "ID" in result.columnsExpected output
Source index: [1, 2, 3, 4, 5, 6]
ID NAME MATH ENGLISH
0 1 Ravi 80 80
1 2 Raju 40 70
2 3 Alex 70 40
3 4 Ron 70 50
4 5 King 70 60
5 6 Jack 30 30The keyword is inplace. The return value is None, so do not assign the method result back to the table.
working = student_data().set_index("NAME")
returned = working.reset_index(inplace=True)
print(working)
print("Return:", returned)
assert returned is None
assert "NAME" in working.columnsExpected output
NAME ID MATH ENGLISH
0 Ravi 1 80 80
1 Raju 2 40 70
2 Alex 3 70 40
3 Ron 4 70 50
4 King 5 70 60
5 Jack 6 30 30
Return: NoneThe sequence here starts at 100. It describes the current row order and can change after filtering or sorting; it is not a persistent record key. rename() changes the inserted column name.
df = student_data().reset_index()
df = df.rename(columns={"index": "New_ID"})
df["New_ID"] = df.index + 100
print(df)
assert df["New_ID"].tolist() == [100, 101, 102, 103, 104, 105]Expected output
New_ID NAME ID MATH ENGLISH
0 100 Ravi 1 80 80
1 101 Raju 2 40 70
2 102 Alex 3 70 40
3 103 Ron 4 70 50
4 104 King 5 70 60
5 105 Jack 6 30 30Keep ID as a normal column and discard only the old positional labels. Filtering does not automatically renumber the remaining rows. Continue with query() and loc.
df = student_data()
selected = df.loc[df["MATH"].ge(70)]
print("Before:", selected.index.tolist())
tidy = selected.reset_index(drop=True)
print(tidy)
assert selected.index.tolist() == [0, 2, 3, 4]
assert tidy.index.tolist() == [0, 1, 2, 3]
assert tidy["ID"].tolist() == [1, 3, 4, 5]Expected output
Before: [0, 2, 3, 4]
NAME ID MATH ENGLISH
0 Ravi 1 80 80
1 Alex 3 70 40
2 Ron 4 70 50
3 King 5 70 60names controls the inserted column label without changing its values. It is useful for preserving source row labels with a descriptive name.
indexed = student_data().set_index("ID")
restored = indexed.reset_index(names="student_id")
print(restored)
assert restored["student_id"].tolist() == [1, 2, 3, 4, 5, 6]Expected output
student_id NAME MATH ENGLISH
0 1 Ravi 80 80
1 2 Raju 40 70
2 3 Alex 70 40
3 4 Ron 70 50
4 5 King 70 60
5 6 Jack 30 30All removed levels become columns unless drop=True. A list supplied to names has one entry per index level.
multi = student_data().set_index(["NAME", "ID"])
restored = multi.reset_index(names=["student_name", "student_id"])
print(restored)
assert restored.columns.tolist() == ["student_name", "student_id", "MATH", "ENGLISH"]
assert restored.index.nlevels == 1Expected output
student_name student_id MATH ENGLISH
0 Ravi 1 80 80
1 Raju 2 40 70
2 Alex 3 70 40
3 Ron 4 70 50
4 King 5 70 60
5 Jack 6 30 30Resetting one level leaves the other levels as the index. This does not necessarily create a sequential RangeIndex.
multi = student_data().set_index(["NAME", "ID"])
partial = multi.reset_index(level="ID")
print(partial)
print("Remaining index name:", partial.index.name)
assert partial.index.name == "NAME"
assert "ID" in partial.columns
assert partial.loc["Raju", "ID"] == 2Expected output
ID MATH ENGLISH
NAME
Ravi 1 80 80
Raju 2 40 70
Alex 3 70 40
Ron 4 70 50
King 5 70 60
Jack 6 30 30
Remaining index name: NAMEThese options affect the column hierarchy, not which row-index levels are reset. Here the restored student label is inserted into column level 1 and labelled key in level 0.
scores = pd.DataFrame(
[[80, 81], [40, 70]],
index=pd.Index(["Ravi", "Raju"], name="student"),
columns=pd.MultiIndex.from_tuples([("score", "math"), ("score", "english")])
)
restored = scores.reset_index(col_level=1, col_fill="key")
print(restored)
print("Column labels:", restored.columns.tolist())
assert ("key", "student") in restored.columns
assert restored[("key", "student")].tolist() == ["Ravi", "Raju"]Expected output
key score
student math english
0 Ravi 80 81
1 Raju 40 70
Column labels: [('key', 'student'), ('score', 'math'), ('score', 'english')]If the index name already exists as a column, resetting can fail. Rename the inserted column with names. If an index duplicates an existing column exactly, dropping that index may also be appropriate after checking the values.
indexed = student_data().set_index("ID", drop=False)
try:
indexed.reset_index()
except ValueError as error:
print(type(error).__name__, str(error))
safe = indexed.reset_index(names="index_ID")
print(safe)
assert safe.columns.is_unique
assert safe["index_ID"].equals(safe["ID"])Expected output
ValueError cannot insert ID, already exists
index_ID NAME ID MATH ENGLISH
0 1 Ravi 1 80 80
1 2 Raju 2 40 70
2 3 Alex 3 70 40
3 4 Ron 4 70 50
4 5 King 5 70 60
5 6 Jack 6 30 30Repeated reset_index() calls can add index and level_0 columns, then encounter a naming conflict. If the labels are disposable, reset with drop=True instead.
df = student_data()
once = df.reset_index()
twice = once.reset_index()
print("Once:", once.columns.tolist())
print("Twice:", twice.columns.tolist())
clean = df.reset_index(drop=True).reset_index(drop=True)
print("Disposable labels:", clean.columns.tolist())
assert "level_0" in twice.columns
assert clean.columns.tolist() == df.columns.tolist()Expected output
Once: ['index', 'NAME', 'ID', 'MATH', 'ENGLISH']
Twice: ['level_0', 'index', 'NAME', 'ID', 'MATH', 'ENGLISH']
Disposable labels: ['NAME', 'ID', 'MATH', 'ENGLISH']Keep dates as a column for a tabular report. See to_datetime(), date_range(), period_range() and date exercises.
daily = pd.DataFrame({
"date": pd.to_datetime(["2026-09-01", "2026-09-03", "2026-10-01"]),
"amount": [20, 35, 40]
}).set_index("date").sort_index()
september = daily.loc["2026-09"].reset_index()
print(september)
assert "date" in september.columns
assert september["amount"].sum() == 55Expected output
date amount
0 2026-09-01 20
1 2026-09-03 35groupby() commonly places group labels in the result index. Reset them into columns before passing a report to an export or merge operation.
sales = pd.DataFrame({
"region": ["North", "North", "South", "South", "North"],
"product": ["Pen", "Book", "Pen", "Book", "Pen"],
"amount": [5, 20, 8, 15, 7]
})
grouped = sales.groupby(["region", "product"]).agg(
orders=("amount", "size"), revenue=("amount", "sum")
)
report = grouped.reset_index()
print(report)
assert report.columns.tolist() == ["region", "product", "orders", "revenue"]
assert report["orders"].sum() == len(sales)
assert report["revenue"].sum() == sales["amount"].sum() == 55Expected output
region product orders revenue
0 North Book 1 20
1 North Pen 2 12
2 South Book 1 15
3 South Pen 1 8After resetting, index=False avoids an extra row-number column. Alternatively, group with as_index=False when you want group labels as columns immediately. See Input and Output and to_excel().
direct = sales.groupby(["region", "product"], as_index=False).agg(
orders=("amount", "size"), revenue=("amount", "sum")
)
print(report.to_csv(index=False).rstrip())
print("Same report:", report.equals(direct))
assert report.equals(direct)
# Optional Colab export:
# report.to_csv("regional_product_sales.csv", index=False)Expected output
region,product,orders,revenue
North,Book,1,20
North,Pen,2,12
South,Book,1,15
South,Pen,1,8
Same report: TrueDoes reset_index() delete duplicates? No; use drop_duplicates() under a defined rule. Does it sort rows? No. Why did an identifier disappear? drop=True discarded index labels. Why is the return value None? inplace=True updates the table. Why is the index still named after a partial reset? Some MultiIndex levels remain. Can I allow duplicate column names? allow_duplicates=True permits them, but resolving the conflict keeps column selection clearer. Will this fix misaligned arithmetic? Arithmetic aligns on labels; decide how records correspond before changing them. Continue with merge(), iloc and Data Cleaning.
Set ID as the index, select students with Maths marks below 50 and restore ID as a column. Predict the IDs, names and resulting index labels.
Raju and Jack have IDs 2 and 6. The restored table has index labels 0 and 1 while retaining those IDs.
indexed = student_data().set_index("ID")
answer = indexed.loc[indexed["MATH"].lt(50)].reset_index()
print(answer)
assert answer["ID"].tolist() == [2, 6]
assert answer["NAME"].tolist() == ["Raju", "Jack"]
assert answer.index.tolist() == [0, 1]Expected output
ID NAME MATH ENGLISH
0 2 Raju 40 70
1 6 Jack 30 30Open in Google Colab View on GitHub
Run the examples in order, change filters and grouping keys, then inspect restored labels and exported reports. 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.reset_index 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.