Encodings and Excel

csv · Practical · 9 min

Mojibake column names and broken accents are the most common CSV support tickets.

CSV files have no way to declare their encoding, so you must know or guess it.

Reading:

  • Modern files are usually UTF-8. Files saved by Excel as "CSV UTF-8" start with a byte order mark (BOM). Opened as utf-8, the first header becomes '\ufeffname' and row["name"] raises KeyError. Use encoding="utf-8-sig", which strips a BOM if present and works fine without one.
  • Older Windows exports may be cp1252; some systems produce latin-1. A UnicodeDecodeError is a strong hint.
  • errors="replace" keeps a script running on bad bytes, at the cost of \ufffd characters; log when it happens.

Writing for Excel:

  • Write encoding="utf-8-sig" so Excel detects UTF-8; without the BOM, Excel on Windows often assumes the local code page and shows accents as garbage.
  • Excel in many European locales expects ; as the delimiter and , as the decimal separator.
  • Excel interprets values: long numbers lose digits, leading zeros vanish, and 2-3 may become a date. That's display-side; the file is fine.

Security: a field starting with =, +, - or @ may be executed as a formula when opened in a spreadsheet (CSV injection). If users supply values for exports, prefix such values with '.

Example

import csv

with open("excel.csv", "w", newline="", encoding="utf-8-sig") as f:   # writes BOM
    csv.writer(f).writerows([["name", "city"], ["José", "Zürich"]])

print(open("excel.csv", "rb").read()[:4])          # b'\xef\xbb\xbfn'  (BOM + 'n')

with open("excel.csv", newline="", encoding="utf-8") as f:
    print(next(csv.DictReader(f)).keys())          # dict_keys(['\ufeffname', 'city'])

with open("excel.csv", newline="", encoding="utf-8-sig") as f:
    print(next(csv.DictReader(f)))                 # {'name': 'José', 'city': 'Zürich'}

def safe_cell(value: str) -> str:
    return "'" + value if value[:1] in ("=", "+", "-", "@") else value
print(safe_cell("=HYPERLINK(\"http://evil\")"))   # '=HYPERLINK("http://evil")

Remember

  • Read with utf-8-sig to strip an Excel BOM automatically
  • Write utf-8-sig when the file is meant for Excel
  • Neutralise leading = + - @ in user data to prevent CSV injection

Watch out for

  • Reading an Excel UTF-8 export with encoding='utf-8' and getting KeyError on the first column.

Check yourself

  1. row['name'] raises KeyError although the header shows 'name'. The file came from Excel. Most likely fix?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/csv.html#module-contents