## TL;DR
The column name has invisible whitespace (`'col '` vs `'col'`), so the lookup misses. Fix it by stripping all column names once at load: `df.columns = df.columns.str.strip()`. It happens because CSV headers and Excel exports routinely carry trailing spaces, tabs, or non-breaking spaces you cannot see.

```text
KeyError: 'customer_id'
# but 'customer_id' is right there in df.columns...
```

## Use this when
- `df['col']` raises KeyError while the name looks correct in `df.columns`
- Column access fails right after `read_csv` or `read_excel`
- Tab-completion shows the column but bracket access fails

## Not for
- Columns that are genuinely absent, check the source
- MultiIndex column selection quirks
- SQL "column does not exist" errors, different skill

## Steps

1. Expose the invisible characters:

```python
print([repr(c) for c in df.columns])
```
Expected output: quoted names like `'customer_id '` with a visible trailing space, or `'\xa0customer'`.

2. Strip whitespace from all column names:

```python
df.columns = df.columns.str.strip()
```
Expected output: clean names; `df['customer_id']` now works.

3. If stripping wasnt enough, check for non-breaking spaces:

```python
df.columns = df.columns.str.replace('\xa0', '', regex=False).str.strip()
```
Expected output: Excel-export artifacts (`\xa0`) removed too.

4. Make it permanent at load time:

```python
df = pd.read_csv('f.csv', skipinitialspace=True)
df.columns = df.columns.str.strip()
```
Expected output: every future load starts clean. Put the strip line in your shared load helper.

5. Guard against case variants too if the source is sloppy:

```python
df.columns = df.columns.str.strip().str.lower()
```
Expected output: `'Customer_ID '` becomes `'customer_id'`. Only do this if downstream code uses lowercase consistently.

## Variant phrasings

### pandas KeyError but column exists
Nine times out of ten it is whitespace. Step 1 with `repr()` proves it in seconds.

### column name has trailing space pandas
`str.strip()` handles spaces and tabs. For `\xa0` (non-breaking space from Excel), use step 3.

### read_csv header whitespace issue
`skipinitialspace=True` handles spaces after delimiters, but trailing spaces in headers still need the strip.

## Why it happens
`'col'` and `'col '` are different strings, and dict-style lookup is exact. Data exports pad headers with spaces, Excel inserts non-breaking spaces, and none of it is visible in a normal `print(df.columns)`. `repr()` is the flashlight.

## Edge cases
- Duplicate names after stripping (`'a'` and `'a '` both become `'a'`): dedupe with `df.columns.duplicated()` and rename.
- Unicode lookalikes (full-width characters, zero-width spaces): normalize with `unicodedata.normalize('NFKC', c)`.
- After cleaning, save the cleaned header mapping somewhere; the next export will have the same dirt.
- `df.rename(columns=str.strip)` works too but `str.strip` on the Index is clearer.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_TLdvRF0UiAf3HDWYqVHUQQ
