Fix KeyError from Spaces in Excel Column Names
The column is right there in the spreadsheet — Net Amount — and the code asks for df["Net Amount"], and pandas raises KeyError: 'Net Amount'. Nothing is misspelled that you can see. The cause is almost always a character you cannot see: a trailing space after the heading, a non-breaking space between the words copied in from a web page or an ERP export, a line break someone typed with Alt+Enter to wrap the heading, or a zero-width space that came along with a paste. To the eye the headings are identical; to Python they are different strings. This page reproduces the error, shows the hidden characters, suggests the column that was meant, and fixes the headings once, right after reading. It belongs to Troubleshooting Common Python Excel Errors.
Prerequisites
pip install pandas openpyxl
The sample workbook has six headings that look ordinary in Excel: Invoice, Customer Name, Net Amount (wrapped onto two lines), VAT, Due–Date and Region.
Step 1 — Reproduce the error and look at the real names
import pandas as pd
df = pd.read_excel("invoices.xlsx")
for wanted in ["Invoice", "Customer Name", "Net Amount", "VAT", "Due-Date"]:
try:
df[wanted]
except KeyError as e:
print("KeyError:", e)
print(list(df.columns))
print([repr(c) for c in df.columns])
Five of the six lookups fail. Printing the list of columns does not help much — the trailing space and the non-breaking space look like ordinary spaces, and the zero-width space shows nothing at all. repr() is the first tool that tells the truth: it shows 'Invoice ' with its trailing space, 'Customer\xa0Name' with the non-breaking space as an escape, 'Net\nAmount' with the line break, and 'VAT' with the zero-width space in front. The dash in Due–Date is an en dash, which repr shows as the character itself, so it still looks like a hyphen unless you know to look for it.
Step 2 — Name every unusual character
import unicodedata
def reveal(name):
odd = [(i, ch, unicodedata.name(ch, f"U+{ord(ch):04X}")) for i, ch in enumerate(name)
if not (ch.isalnum() or ch == " ") or (ch == " " and (i == 0 or i == len(name) - 1))]
return odd
for col in df.columns:
found = reveal(col)
if found:
print(f"{col!r:22}", "; ".join(f"pos {i}: {n}" for i, _, n in found))
unicodedata.name() turns each suspicious character into its official name, which removes all guesswork: NO-BREAK SPACE, ZERO WIDTH SPACE, EN DASH, and an ordinary SPACE at the end of Invoice. Control characters such as the line feed from Alt+Enter have no name in the Unicode database, so the function falls back to the code point, U+000A; the common ones are worth recognising on sight — U+000A is a line feed, U+000D a carriage return, U+0009 a tab. The function flags anything that is not a letter, digit or interior space, plus spaces at either end. Running it over the columns of a misbehaving file takes a second and usually ends the investigation, and it is worth keeping in a utilities module for exactly that purpose.
Step 3 — Normalise the headings once, right after reading
import re
def clean_header(name):
text = unicodedata.normalize("NFKC", str(name)) # NBSP -> space, full-width -> normal, etc.
text = re.sub(r"[-]", "", text) # zero-width characters and BOM
text = re.sub(r"[‐-―−]", "-", text) # hyphen and dash variants -> '-'
text = re.sub(r"\s+", " ", text) # line breaks, tabs, runs of spaces
return text.strip()
df = pd.read_excel("invoices.xlsx").rename(columns=clean_header)
print([repr(c) for c in df.columns])
print(df[["Invoice", "Customer Name", "Net Amount", "VAT", "Due-Date"]].to_string(index=False))
One function applied once, immediately after read_excel, fixes every heading for the rest of the program. NFKC normalisation is the heavy lifter: it turns the non-breaking space into an ordinary space and also folds other look-alikes, such as full-width letters from East Asian input methods, into their plain forms. It does not remove zero-width characters or change dashes, so two small substitutions handle those. Collapsing all whitespace — including the line break from Alt+Enter — into single spaces and stripping the ends finishes the job. After that, all five lookups work, and the printed repr of every column shows plain names with nothing hidden in them. Applying the function with rename(columns=...) keeps the order of columns and touches nothing else in the DataFrame.
Step 4 — Make the next KeyError explain itself
import difflib
def col(df, name):
"""Look up a column, and on failure say which existing column was probably meant."""
if name in df.columns:
return df[name]
cleaned = {clean_header(c): c for c in df.columns} # clean form -> real column name
close = difflib.get_close_matches(clean_header(name), list(cleaned), n=1, cutoff=0.6)
hint = f" Did you mean {cleaned[close[0]]!r}?" if close else ""
raise KeyError(f"No column {name!r}. Columns are: {list(df.columns)}.{hint}") from None
raw = pd.read_excel("invoices.xlsx")
try:
col(raw, "Customer Name")
except KeyError as e:
print(e)
Even with clean headings, a column can be renamed at source — Customer Name becomes Customer next quarter. A small lookup helper that lists the available columns and suggests the closest one turns a bare KeyError into a message that says what to change. difflib.get_close_matches compares the cleaned versions, so it finds the intended column even when the only difference is an invisible character, which is exactly the case where a human would struggle. The suggestion is then shown as the real column name with repr, so the message itself reveals the culprit: Did you mean 'Customer\xa0Name'? points straight at the non-breaking space. For a full check of all expected columns before processing starts, see Validate Excel Columns Before Import with pandas.
Where the characters come from
Knowing the source helps prevent the problem upstream. Trailing spaces come from people typing; they are the most common cause and the easiest to miss. Non-breaking spaces come from copying text out of web pages, PDFs and many ERP and CRM exports, which use them to stop headings wrapping. Line breaks appear when someone wraps a long heading with Alt+Enter to make a narrow column look tidy. Zero-width spaces and byte-order marks ride along with text pasted from websites and chat tools, or sit at the very start of CSV files saved by some applications. En and em dashes are introduced by autocorrect in Word and Outlook and then pasted into Excel. None of them is visible in a cell, which is why the fix has to be in code. The same problems affect values as well as headings — a customer called Acme Ltd with a trailing non-breaking space will not match in a merge — and the same cleaning function can be applied to key columns, as described in Merge on Keys That Differ in Case and Spacing with pandas.
Prevent it in the files you produce
When your own code writes workbooks that other scripts read, write clean headings in the first place: apply the same function to the DataFrame's columns before to_excel, and avoid wrapping headings with embedded line breaks — set wrap_text on the header cells instead, which wraps the display without changing the text. When a source system keeps producing odd headings, it is often worth a short message to whoever owns the export; a trailing space in a report template takes seconds to remove at source and saves every consumer from working around it.
Common pitfalls
- Printing the columns list. It hides most invisible characters; use
repr()andunicodedata.name(). - Only calling
.str.strip(). It does not remove no-break or zero-width spaces between words. - Cleaning in several places. Normalise once after reading and use the clean names everywhere.
- Forgetting dashes. An en dash looks like a hyphen; map dash variants to
-. - Bare KeyErrors. List the available columns and suggest the closest match.
Conclusion
A KeyError for a column you can see means the heading contains a character you cannot: a trailing space, a non-breaking space, a line break, a zero-width space or a different dash. Reveal them with repr() and unicodedata.name(), normalise every heading once after read_excel with NFKC plus a few substitutions, and make lookups fail with the list of real columns and the nearest match.
Frequently asked questions
Why does pandas say KeyError when the column exists in Excel? The heading in the file contains an invisible character, such as a trailing or non-breaking space or a line break, so the string differs from the one in your code.
How do I see hidden characters in pandas column names?
Print [repr(c) for c in df.columns], and use unicodedata.name(ch) on unusual characters to identify them.
How do I remove non-breaking spaces from column names?
Apply unicodedata.normalize("NFKC", name), which turns them into ordinary spaces, then collapse and strip whitespace.
Does df.columns.str.strip() fix it? Only for ordinary spaces at the ends; it leaves non-breaking spaces between words, zero-width characters and line breaks inside the name.
Related
- Up one level: Troubleshooting Common Python Excel Errors — common errors and fixes.
- Standardise Excel Column Names with pandas — consistent naming conventions.
- Fix Duplicate Column Names When Reading Excel with pandas — another header problem.
- Fix "Unnamed: 0" Columns When Reading Excel with pandas — headers in the wrong place.
More on errors & fixes
- FixHandle Permission Denied When Writing Excel in Python
- How-toWhat openpyxl Loses When You Save an Existing Workbook
- FixFix Excel Serial Numbers Showing Instead of Dates
- How-toHandle Daylight Saving Time in Excel Timestamps
- How-toHandle Microsoft Graph Throttling in Excel Automation
- GuideError Handling and Logging in Excel Automation