Merging and Joining Pandas DataFrames: A Complete Guide (2026)
Merging and Joining Pandas Dataframes into ab table on a shared id column

🐍 Python Cluster · 8 of 15 · Intermediate

Merging and Joining Pandas DataFrames: A Complete Guide (2026)

Merging and Joining Pandas DataFrames is how separate tables that share a key, a customer list here, an order list there, become one usable dataset. Lesson 7 covered how to summarize data once it is together. This guide covers getting it together in the first place: the four join types, the real difference between merge(), join(), and concat(), and the row-explosion mistake that catches almost everyone at least once.

by Khalid Hussain Published Aug 12, 2026 🕐 13 min read

Quick Answer: Merging and Joining Pandas DataFrames

▶ Direct Answer · Merging and Joining Pandas DataFrames

Merging and joining Pandas DataFrames combines two tables that share a key, a column or an index, into one, the same job a SQL join does. Use .merge() for column based, SQL style joins with a choice of inner, left, right, or outer. Use .join() as a shortcut when the key is the index, which defaults to a left join. Use .concat() only when the tables are already aligned and just need to be stacked, since it does not match on a key at all.

4
join types, inner, left, right, and outer, all set with one how parameter
Covered in full below
3
tools that look similar but do different jobs: merge(), join(), and concat()
Resolved below
1
argument, validate, that turns a silent row count mistake into an error
See Safety Nets

Wrong Tool, Wrong Table

A customer list and an order list share one thing: a customer_id column. The instinct might be to reach for pd.concat() to put them together.

pythonTHE_WRONG_TOOL.PY
import pandas as pd

customers = pd.DataFrame({
    "customer_id": [1, 2, 3],
    "name": ["Alex", "Sam", "Priya"]
})

orders = pd.DataFrame({
    "customer_id": [1, 1, 2, 4],
    "item": ["mug", "pen", "notebook", "mug"],
    "amount": [15.0, 5.0, 10.0, 8.0]
})

pd.concat([customers, orders])

The result is not what anyone actually wants. concat() never looks at customer_id as a key. It just stacks the two tables on top of each other, lining up whatever column names happen to match and filling the rest with missing values. Three customer rows and four order rows become seven ragged rows, none of them actually combined.

What the table really needs is a merge, matching each order to its customer through the shared customer_id column:

pythonTHE_RIGHT_TOOL.PY
customers.merge(orders, on="customer_id")
Carried over from Articles 6 and 7

A merged or joined DataFrame is still just a DataFrame, so Pandas 3.0's copy on write default applies here too. The result of a merge is always independent of both original tables, never a live view back into either one.

The Four Join Types

The how parameter controls which rows survive when a key does not appear on both sides. Using the same customers and orders tables from above, where Priya has no orders and one order belongs to a customer_id that does not exist:

pythonINNER_JOIN.PY
customers.merge(orders, on="customer_id", how="inner")
#    customer_id   name      item  amount
# 0            1   Alex       mug    15.0
# 1            1   Alex       pen     5.0
# 2            2    Sam  notebook    10.0

Only customer_id values present on both sides survive. Priya (no orders) and the orphan order for customer_id 4 both disappear.

pythonLEFT_JOIN.PY
customers.merge(orders, on="customer_id", how="left")
#    customer_id   name      item  amount
# 0            1   Alex       mug    15.0
# 1            1   Alex       pen     5.0
# 2            2    Sam  notebook    10.0
# 3            3  Priya       NaN     NaN

Every customer survives, matched or not. Priya now appears with NaN in the order columns. The orphan order is still dropped, since it has no match in the left table.

pythonRIGHT_AND_OUTER.PY
customers.merge(orders, on="customer_id", how="right")   # every order survives; Priya drops out
customers.merge(orders, on="customer_id", how="outer")  # nothing is dropped on either side

right keeps every order, including the one from customer_id 4, which now shows NaN for name. outer keeps everything from both tables at once, five rows in total: nothing is lost either direction.

merge vs. join vs. concat

MethodMatches onDefault
.merge()a shared column, or the indexinner
.join()the index (or a column via on)left
.concat()nothing, just position or index alignmentno matching at all

.join() is really a convenience wrapper around merge, built for the common case of combining on the index instead of a named column:

pythonJOIN_SHORTCUT.PY
customers.set_index("customer_id").join(orders.set_index("customer_id"))
Easy to trip on

That line quietly performs a left join, since .join() defaults to left while .merge() defaults to inner. Running the equivalent .merge() without specifying how would drop Priya's row instead of keeping it with NaN values, the exact kind of silent difference that changes a row count without raising any error.

Safety Nets: validate and indicator

Two arguments turn a merge from a guess into something checked. validate states the relationship the code assumes and raises an error if the data does not actually match it:

pythonVALIDATE.PY
customers.merge(orders, on="customer_id", how="left", validate="one_to_many")

If customer_id unexpectedly turned out to have duplicates in the customers table, this line would raise a MergeError immediately instead of silently returning a distorted result. indicator is the diagnostic counterpart, adding a column that labels exactly where each row came from:

pythonINDICATOR.PY
customers.merge(orders, on="customer_id", how="outer", indicator=True)
# adds a _merge column: "both", "left_only", or "right_only"

The Row Explosion Gotcha

Every example above matches at most one order row per customer at a time. Real data is not always that tidy. When a key is duplicated on both sides of a merge, pandas returns every possible combination of the matching rows, not just one. Two duplicate keys on the left times three on the right produces six rows, not two or three. On a large table, that kind of many-to-many merge can multiply row counts fast enough to exhaust memory before anyone notices the mistake.

validate="one_to_many" or validate="many_to_one", matching whichever relationship the data is actually supposed to have, is the direct defense against this: pandas checks key uniqueness before the merge runs, and stops with an error rather than quietly returning a table with far more rows than expected.

Mistakes That Give Away a Beginner

  • Reaching for .concat() when the tables need to be matched on a key, not just stacked, the exact mistake in this article's hook.
  • Forgetting .join() defaults to left while .merge() defaults to inner, then being surprised by a different row count between the two.
  • Skipping validate and not noticing a merge quietly multiplied rows through an unexpected many-to-many match.
  • Assuming missing keys never match. Pandas matches null keys against each other by default, unlike standard SQL join behavior.
  • Leaving the default _x and _y suffixes on overlapping column names instead of renaming them, making the result harder to read later.

Practice: Fix These Three Yourself

1

All four

Run all four join types, inner, left, right, and outer, on the customers and orders tables, and note how the row count changes each time.

2

Catch it early

Add validate="one_to_many" to the merge and confirm it runs cleanly. Then duplicate a customer_id in the customers table on purpose and watch the error appear.

3

Where did it go

Use indicator=True on an outer merge, then filter the result down to only the rows where _merge is not "both".

Frequently Asked Questions

What's the difference between merge, join, and concat in pandas?

merge() combines two DataFrames by matching values in a shared column or index, similar to a SQL join. join() is a shortcut for merging on the index, defaulting to a left join. concat() simply stacks DataFrames along rows or columns without matching on any key at all.

What are the four main types of joins in pandas?

Inner, left, right, and outer, set with the how parameter in merge(). Inner keeps only matching rows, left keeps all rows from the left table, right keeps all rows from the right table, and outer keeps everything from both.

Does pandas join default to inner or left?

join() defaults to left. merge() defaults to inner. Mixing up the two is a common source of unexpected row counts.

How do you avoid duplicate rows after a pandas merge?

Check for duplicate keys before merging, and pass validate, such as validate="one_to_one", so pandas raises an error instead of silently multiplying rows when a key is not actually unique.

What does the validate parameter do in pandas merge?

It checks the merge against an assumed relationship, one_to_one, one_to_many, many_to_one, or many_to_many, and raises a MergeError if the actual data does not match.

Is pandas merge the same as a SQL join?

Mostly. merge() implements the same inner, left, right, and outer logic as SQL joins, with one notable difference: pandas matches null keys against each other by default, which standard SQL does not do.

Conclusion: One Key, Four Outcomes

Four join types and three tools, all built around one idea: combining data that lives in separate tables because it shares a key. .merge() and .join() do the real matching work, while .concat() only stacks tables that are already aligned. validate and indicator turn a silent mismatch into an error or a labeled row instead of a table that quietly has the wrong number of rows in it.

Practice the three exercises above, then move to the next article in this cluster, which covers list comprehensions and generator expressions: a faster, more compact way to write the loops that built these tables in the first place.

🐍 Continue the Python Cluster

Next Up: List Comprehensions and Generator Expressions

Faster, cleaner loops, and when a generator beats a list.

▶  Read the Comprehensions Guide