join(): Index-Based Joining
Use DataFrame.join() to attach columns from other DataFrames/Series by matching index labels — how=, lsuffix/rsuffix, on=, and joining several frames at once.
Watch the lesson
1 video on CodeWithMunnaX
When your tables are already indexed by the key (player name as the index — common after set_index or groupby), there is a shorter tool than merge: DataFrame.join(). It lines rows up by index label and defaults to a left join.
python
runs = pd.DataFrame({"runs": [741, 417, 890, 446]},
index=pd.Index(["Kohli", "Rohit", "Gill", "Pant"], name="player"))
team = pd.DataFrame({"team": ["RCB", "MI", "GT", "MI"]},
index=pd.Index(["Kohli", "Rohit", "Gill", "Bumrah"], name="player"))Definition
DataFrame.join()adds the columns of one or more other DataFrames/named Series to the calling DataFrame by matching index labels (or a column of the caller against the other's index withon=). It is a convenience wrapper aroundmerge(left_index=True, right_index=True)withhow="left"as the default.
- Calling DataFrame — the one before the dot (
runs.join(...)) is the left table. - Index labels — the keys are the row labels, not a column.
- Left by default — all rows of the caller are kept (merge's default is inner!).
Syntax
python
df.join(other, on=None, how="left", lsuffix="", rsuffix="", sort=False, validate=None)| Parameter | Meaning | Default |
|---|---|---|
other | a DataFrame, a named Series, or a list of DataFrames | required |
on | a column of the caller to match against other's index | None (use caller's index) |
how | "left", "right", "inner", "outer", "cross" | "left" |
lsuffix / rsuffix | suffixes for overlapping column names — required if any column names overlap | "" |
sort | sort the result by the join key | False |
validate | same checks as merge ("one_to_one", "many_to_one"…) | None |
Example — default left join on the index
python
print(runs.join(team))text
runs team
player
Kohli 741 RCB
Rohit 417 MI
Gill 890 GT
Pant 446 NaNHow it works: every row of runs is kept; team is looked up by index label. Pant has no label in team → NaN; Bumrah is ignored. Other how values behave exactly as in merge:
python
print(runs.join(team, how="outer"))text
runs team
player
Bumrah NaN MI
Gill 890.0 GT
Kohli 741.0 RCB
Pant 446.0 NaN
Rohit 417.0 MIExample — overlapping columns need lsuffix / rsuffix
python
r24 = pd.DataFrame({"runs": [741, 426]}, index=["Kohli", "Gill"])
r23 = pd.DataFrame({"runs": [639, 890]}, index=["Kohli", "Gill"])
r24.join(r23)text
ValueError: columns overlap but no suffix specified: Index(['runs'], dtype='str')python
print(r24.join(r23, lsuffix="_2024", rsuffix="_2023"))text
runs_2024 runs_2023
Kohli 741 639
Gill 426 890Unlike merge (which adds _x/_y automatically), join refuses to guess — you must supply suffixes.
Example — on=: a column of the caller vs the other's index
python
bat = pd.DataFrame({"player": ["Kohli", "Rohit", "Pant"], "runs": [741, 417, 446]})
print(bat.join(team, on="player"))text
player runs team
0 Kohli 741 RCB
1 Rohit 417 MI
2 Pant 446 NaNThe caller keeps its normal 0, 1, 2 index; its player column is matched against team's index.
Example — several frames (and Series) at once
python
sixes = pd.DataFrame({"sixes": [38, 23, 33]}, index=["Kohli", "Rohit", "Gill"])
print(runs.join([team, sixes]))
matches = pd.Series([15, 14, 17], index=["Kohli", "Rohit", "Gill"], name="matches")
print(runs.join(matches))text
runs team sixes
player
Kohli 741.0 RCB 38.0
Rohit 417.0 MI 23.0
Gill 890.0 GT 33.0
Pant 446.0 NaN NaN
runs matches
player
Kohli 741 15.0
Rohit 417 14.0
Gill 890 17.0
Pant 446 NaNA list joins everything in one call (here runs happened to come back as float). A Series must have a name — it becomes the new column's name.
Notebook 10's example — default RangeIndex
python
df1 = pd.DataFrame({"Name": ["Aman", "Rohit"], "Marks": [80, 85]})
df2 = pd.DataFrame({"City": ["Delhi", "Mumbai"]})
print(df1.join(df2))text
Name Marks City
0 Aman 80 Delhi
1 Rohit 85 MumbaiBoth frames have the default index 0, 1, so row 0 meets row 0. This only works because both are in the same order — with real data, join on a meaningful index (or merge on a key column).
join vs merge
df.join(other) | df.merge(other) / pd.merge | |
|---|---|---|
| Matches on | index (or caller column → other index with on=) | columns (or index with left_index/right_index) |
Default how | left | inner |
| Overlapping columns | must give lsuffix/rsuffix (else error) | automatic _x/_y |
| Many frames at once | ✅ pass a list | ❌ one at a time |
indicator | ❌ | ✅ |
| Result index | caller's index | new 0..n-1 index (column joins) |
runs.join(team, how="left") equals runs.merge(team, left_index=True, right_index=True, how="left").
Common mistakes
- ❌ Calling
joinon frames with a default 0..n-1 index and assuming it matches by player. ✅set_index("player")first, or usemerge(on="player"). - ❌ Forgetting
lsuffix/rsuffixwith overlapping columns →ValueError. ✅ Supply both. - ❌ Expecting join to be inner like merge. ✅
joindefaults tohow="left". - ❌ Joining an unnamed Series. ✅ Give it a
name(s.rename("matches")).
Interview tip
Q: When would you choose join over merge?
A: When the key is already the index on both sides (e.g. results of groupby), or you want to attach several indexed tables in one call. join is index-based and left by default; merge is column-based, inner by default, and offers indicator.
Key points to remember
- DataFrame.join() combines frames by matching index labels — a shortcut for merge(left_index=True, right_index=True).
- join's default is how='left' (merge's default is inner).
- Overlapping column names raise ValueError unless lsuffix/rsuffix are given.
- on='col' matches a column of the calling frame against the other frame's index.
- join accepts a list of DataFrames to attach several tables at once; a Series must have a name.
- With default RangeIndexes, join just matches by position-like labels 0, 1, 2 — only safe if rows are in the same order.
Try it yourself
Join indexed tables
Suffixes, on= and joining a list
Practice: join groupby results
Revision Flashcards
Tap a card to flip it and see the answer.
