Concatenating & Appending
Reviewed & published by Brayan K
Concatenation in pandas is the operation of gluing DataFrames together along an axis — stacking them vertically to add more rows or side-by-side to add more columns — using pd.concat(), the modern replacement for the removed df.append() method.
Part of the free Pandas course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Stacking DataFrames vertically with axis=0
- Combining DataFrames side-by-side with axis=1
- Resetting the index with ignore_index=True
- Labelling pieces with keys=[...] for a MultiIndex
- Why df.append was removed in pandas 2.0
- How concat aligns columns and fills gaps with NaN
1 Stacking Rows — Vertical Concat (axis=0)
The classic use case: you have the same data split across several files (say, one CSV per month) and you want a single combined table. pd.concat([df1, df2]) stacks them on top of each other.
By default it keeps the original index labels, which is why you often pass ignore_index=True for a clean 0..n-1 index.
import pandas as pd
jan = pd.DataFrame({
"name": ["Alice", "Bob"],
"sales": [100, 200],
})
feb = pd.DataFrame({
"name": ["Carol", "Dan"],
"sales": [150, 250],
})
# Stack feb under jan
combined = pd.concat([jan, feb])
print(combined)
# Note the index: 0, 1, 0, 1 (labels carried over from each frame)
print("---")
# ignore_index=True renumbers cleanly: 0, 1, 2, 3
clean = pd.concat([jan, feb], ignore_index=True)
print(clean)
# ✅ Expected output:
# name sales
# 0 Alice 100
# 1 Bob 200
# 0 Carol 150
# 1 Dan 250
# ---
# name sales
# 0 Alice 100
# 1 Bob 200
# 2 Carol 150
# 3 Dan 2502 Aligning Columns & Going Horizontal (axis=1)
When you stack vertically, pandas aligns by column name. If one frame has a column the other lacks, the missing cells become NaN. Setting axis=1 instead glues frames side-by-side, aligning by the index.
import pandas as pd
a = pd.DataFrame({"name": ["Alice", "Bob"], "sales": [100, 200]})
b = pd.DataFrame({"name": ["Carol", "Dan"], "region": ["North", "South"]})
# Vertical: columns are aligned by name; mismatches become NaN
v = pd.concat([a, b], ignore_index=True)
print(v)
# 'sales' is NaN for Carol/Dan; 'region' is NaN for Alice/Bob.
print("---")
# Horizontal (axis=1): glue side-by-side, aligned on the index
left = pd.DataFrame({"sales": [100, 200]}, index=["Alice", "Bob"])
right = pd.DataFrame({"region": ["North", "South"]}, index=["Alice", "Bob"])
h = pd.concat([left, right], axis=1)
print(h)
# One row per person with both 'sales' and 'region' columns.
# ✅ Expected output:
# name sales region
# 0 Alice 100.0 NaN
# 1 Bob 200.0 NaN
# 2 Carol NaN North
# 3 Dan NaN South
# ---
# sales region
# Alice 100 North
# Bob 200 South3 Labelling Each Piece with keys=[...]
Often you want to remember which source each row came from. Passing keys=[...] wraps the result in a hierarchical (Multi)Index whose outer level is the label you supplied.
import pandas as pd
q1 = pd.DataFrame({"product": ["Pen", "Pad"], "units": [30, 40]})
q2 = pd.DataFrame({"product": ["Pen", "Pad"], "units": [50, 20]})
# keys add an outer index level labelling each source frame
report = pd.concat([q1, q2], keys=["Q1", "Q2"])
print(report)
# The index is now a MultiIndex: (Q1, 0), (Q1, 1), (Q2, 0), (Q2, 1)
print("---")
# You can pull a single piece back out by its key
print(report.loc["Q2"])
# Just the Q2 rows.
# ✅ Expected output:
# product units
# Q1 0 Pen 30
# 1 Pad 40
# Q2 0 Pen 50
# 1 Pad 20
# ---
# product units
# 0 Pen 50
# 1 Pad 20🎯 Your Turn: Combine and Renumber
Stack the two frames and give the result a clean 0,1,2,3 index. Fill in the blank argument.
import pandas as pd
week1 = pd.DataFrame({"day": ["Mon", "Tue"], "steps": [8000, 9000]})
week2 = pd.DataFrame({"day": ["Wed", "Thu"], "steps": [7500, 10000]})
# 🎯 YOUR TURN: stack them with a fresh 0..3 index
result = pd.concat([week1, week2], ___=True)
print(result)
# EXPECTED OUTPUT:
# day steps
# 0 Mon 8000
# 1 Tue 9000
# 2 Wed 7500
# 3 Thu 10000📋 Quick Reference
| Task | Code |
|---|---|
| Stack rows | pd.concat([a, b]) |
| Clean index | pd.concat([a, b], ignore_index=True) |
| Side-by-side | pd.concat([a, b], axis=1) |
| Label pieces | pd.concat([a, b], keys=["x", "y"]) |
| Old (removed) | a.append(b) → use concat |
🏆 Mini Challenge: Merge Three Monthly Files
You receive three monthly DataFrames. Combine them into one, tag each row with its month using keys, then total sales per month.
import pandas as pd
jan = pd.DataFrame({"rep": ["A", "B"], "sales": [100, 120]})
feb = pd.DataFrame({"rep": ["A", "B"], "sales": [90, 140]})
mar = pd.DataFrame({"rep": ["A", "B"], "sales": [130, 110]})
# 1) Concat with month labels as the outer index level
year = pd.concat([jan, feb, mar], keys=["Jan", "Feb", "Mar"])
print(year)
# 2) The outer index level (level=0) is the month -> total per month
monthly = year.groupby(level=0)["sales"].sum()
print(monthly)
# EXPECTED: Jan=220, Feb=230, Mar=240
# ✅ Expected output:
# rep sales
# Jan 0 A 100
# 1 B 120
# Feb 0 A 90
# 1 B 140
# Mar 0 A 130
# 1 B 110
# Feb 230
# Jan 220
# Mar 240
# Name: sales, dtype: int64❓ Frequently Asked Questions
Lesson complete — you can stack data any direction!
You can concatenate frames vertically or horizontally, renumber the index, label each piece with keys, and you know to reach for concat instead of the removed append. Combined with merge, you can assemble data from any number of sources.
Practice quiz
What does pd.concat do?
- Matches rows by shared key values
- Sorts a DataFrame
- Stacks DataFrames along an axis
- Removes duplicate columns
Answer: Stacks DataFrames along an axis. concat glues frames together along an axis; it does not match on key values like merge.
What is the default axis for pd.concat?
- axis=0 (stack rows vertically)
- axis=1 (stack columns)
- axis=2
- There is no default
Answer: axis=0 (stack rows vertically). The default axis=0 stacks frames vertically, adding more rows.
What does axis=1 do in pd.concat?
- Stacks rows
- Deletes columns
- Sorts the index
- Glues frames side-by-side, aligning on the index
Answer: Glues frames side-by-side, aligning on the index. axis=1 places frames side-by-side and aligns them by the index.
After pd.concat([jan, feb]) where each frame had index 0,1, what is the result index?
- [0, 1, 2, 3]
- [0, 1, 0, 1]
- [1, 2, 3, 4]
- An error
Answer: [0, 1, 0, 1]. By default the original index labels are kept, giving 0, 1, 0, 1.
What does ignore_index=True produce for that same concat?
- [0, 1, 2, 3]
- [0, 1, 0, 1]
- The original index
- Reversed order
Answer: [0, 1, 2, 3]. ignore_index=True renumbers the result as a clean 0..n-1 RangeIndex.
During a vertical concat, how does pandas align the frames?
- By position only
- By data type
- By column name, filling missing cells with NaN
- It refuses mismatched columns
Answer: By column name, filling missing cells with NaN. Columns align by name; any column missing in one frame becomes NaN there.
What does the keys=['Q1','Q2'] argument add?
- A new data column
- An outer MultiIndex level labelling each source frame
- Sorting by key
- A duplicate check
Answer: An outer MultiIndex level labelling each source frame. keys wraps the result in a hierarchical index whose outer level is the supplied label.
Why was DataFrame.append removed in pandas 2.0?
- It was renamed concat
- It only worked on Series
- It corrupted the index
- It was inefficient, creating a new DataFrame on every call
Answer: It was inefficient, creating a new DataFrame on every call. append rebuilt the whole frame each call; pd.concat once at the end is the efficient replacement.
What is the difference between pd.concat and pd.merge?
- They are the same
- concat stacks similarly-shaped data; merge joins by matching key values
- merge stacks rows; concat joins keys
- concat only works on Series
Answer: concat stacks similarly-shaped data; merge joins by matching key values. Use concat to pile up similar data along an axis; use merge to relate tables by a key.
To total sales per source after concat with keys, what works?
- drop_duplicates()
- sort_values()
- groupby(level=0)['sales'].sum()
- reset_index() only
Answer: groupby(level=0)['sales'].sum(). The outer index level (level=0) holds the key, so grouping by it sums per source.
Continue this course
- Previous: Merging & Joining DataFrames
- Next: Pivot Tables & Crosstab — Reshape and summarise data into spreadsheet-style pivot tables
- Quick reference: Pandas cheat sheet › Merging & Reshaping
Frequently asked questions
What is the difference between pd.concat and pd.merge?
pd.concat stacks DataFrames together along an axis — vertically (more rows) or horizontally (more columns) — aligning on the other axis's labels. It does NOT match on key values. pd.merge is for relational joins that match rows by shared key values. Use concat to pile up similarly-shaped data; use merge to relate different tables by a key.
Why is df.append gone in pandas 2.0?
The DataFrame.append method was deprecated and then removed in pandas 2.0 because it was inefficient — it created a brand-new DataFrame on every call. The replacement is pd.concat([df1, df2]). If you need to build a frame from many pieces, collect them in a list and call pd.concat once at the end.
When should I use ignore_index=True?
Use ignore_index=True for a vertical concat when the original index labels are meaningless (for example, both frames start at 0) and you want a clean 0..n-1 RangeIndex in the result. Leave it off when the index carries information you want to keep, such as dates or IDs.