## TL;DR
Normalize the merge keys before merging: convert both sides to the same dtype, strip whitespace, and unify case. Merge key mismatches happen because pandas matches keys exactly, so int vs str keys or "ABC " vs "ABC" never join, and strict `validate=` merges raise MergeError when the keys arent unique on the side you claimed.

```text
pandas MergeError: how to fix merge key mismatches
```

## Use this when
- `pd.merge` raises MergeError about key dtypes or uniqueness
- The merge returns far fewer rows than expected, or zero
- Keys look the same when printed but dont match

## Not for this skill when
- The merge runs but the row count explodes, thats a non-unique-key fanout problem
- You are joining in SQL, not pandas
- The merge is just slow on big data, thats a performance problem

## Steps

1. Compare the key dtypes on both sides. This is the cause most of the time:

```python
print(df_left["user_id"].dtype, df_right["user_id"].dtype)
print(df_left["user_id"].head(3).tolist(), df_right["user_id"].head(3).tolist())
```
Expected output: something like `int64 object` with values `[101, 102, 103]` vs `['101', '102', '103']`. pandas will not coerce int to str for you, so nothing matches.

2. Convert both keys to one common dtype. String is the safest when either side has leading zeros or mixed content:

```python
df_left["user_id"] = df_left["user_id"].astype(str)
df_right["user_id"] = df_right["user_id"].astype(str)
merged = pd.merge(df_left, df_right, on="user_id", how="inner")
print(len(merged))
```
Expected output: a nonzero row count. If one side is genuinely numeric and clean, `pd.to_numeric` on the string side works too.

3. Normalize string keys: strip whitespace and unify case. Invisible differences are the second most common cause:

```python
for df in (df_left, df_right):
    df["user_id"] = df["user_id"].astype(str).str.strip().str.lower()
```
Expected output: keys like `"ABC "` and `"abc"` become identical `"abc"` and start matching.

4. Check for nulls in the keys. NaN never equals NaN in a merge, so those rows silently vanish:

```python
print(df_left["user_id"].isna().sum(), df_right["user_id"].isna().sum())
```
Expected output: the count of key values that can never match. Decide whether to drop them or fill them before merging.

5. Add `indicator=True` to see exactly which rows failed to match:

```python
m = pd.merge(df_left, df_right, on="user_id", how="outer", indicator=True)
print(m["_merge"].value_counts())
print(m[m["_merge"] != "both"].head())
```
Expected output: counts of `left_only`, `right_only`, and `both`, plus the offending rows. This tells you whether the problem is on one side or both.

6. Once keys are clean, assert the cardinality you expect with `validate=`:

```python
merged = pd.merge(df_left, df_right, on="user_id", validate="many_to_one")
```
Expected output: the merge succeeds, or a MergeError naming the side with duplicate keys. That error is now useful instead of mysterious, because the keys themselves are clean.

## Variant phrasings

### pandas merge returns empty dataframe but keys match
Almost always a dtype mismatch (step 1) or hidden whitespace (step 3). Print `.dtype` before you do anything else.

### MergeError: Merge keys are not unique in either left or right dataset
Your `validate="one_to_one"` claim was wrong. Check duplicates per side with `df.duplicated(subset="key").sum()` and either dedupe or relax the validate argument to match reality.

### pandas merge on int64 and object columns error
Convert both to the same dtype (step 2). If the string side has non-numeric junk, `pd.to_numeric(..., errors="coerce")` will surface it as NaN instead of crashing.

## Why it happens
pandas merge keys are compared with exact equality after a dtype-compatibility check. Two columns that print identically can differ in dtype (int64 vs object), in hidden characters (trailing spaces, non-breaking spaces), or in case, and none of those will ever match. The `validate=` parameter adds a uniqueness assertion on top, which raises MergeError when the data contradicts the claimed cardinality.

## Edge cases
- Datetime keys with mixed timezone awareness (naive vs aware) never match; localize both first.
- Float keys like `101.0` vs int `101` mismatch; round and cast to a nullable integer dtype before merging.
- Merging on multiple keys: all of them must match on dtype and normalization, check each one.
- Categorical keys only match when the categories are identical; convert to str on both sides to sidestep it.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_WSnNowK-LDS589_FPQJabA
