Fix pandas "UnicodeDecodeError: 'utf-8' codec can't decode byte"

Fix pandas "UnicodeDecodeError: 'utf-8' codec can't decode byte"

This one usually shows up the first time a CSV comes from Excel or another Windows tool instead of being generated by your own UTF-8-only pipeline. The instinct is to reach for errors="replace" and move on, but that quietly deletes data instead of fixing anything. This article reproduces the error, explains why it's almost always a single specific encoding mismatch, and shows how to detect and fix it properly.

Reproducing the Error

Any CSV containing accented or non-ASCII characters, saved in a single-byte encoding other than UTF-8, triggers it the moment read_csv() tries to decode those bytes as UTF-8:

import pandas as pd

# A CSV saved as Windows-1252/Latin-1, e.g. exported from Excel
# name,city
# José,Bogotá
# François,Zürich

df = pd.read_csv("data.csv")
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 13: invalid continuation byte

The byte position in the error message points at the first accented character pandas' C parser hit while decoding, not necessarily the first one in the file (the parser reads in chunks), which is why the position often looks like it's "in the middle" of a file that has non-ASCII characters near the top.

The Real Cause: UTF-8 Assumed, Windows-1252 Actual

pd.read_csv() defaults to encoding="utf-8". UTF-8 encodes any character outside plain ASCII using two or more bytes, following a specific bit-pattern rule for continuation bytes. Windows-1252 and Latin-1 (ISO-8859-1) instead encode common Western European accented characters as a single byte in the upper range (0x80-0xFF). A UTF-8 decoder reading one of those single bytes sees an invalid continuation-byte pattern and raises, rather than silently misreading it as some other character:

"é".encode("utf-8")     # b'\xc3\xa9' — two bytes, valid UTF-8 continuation sequence
"é".encode("cp1252")    # b'\xe9'      — one byte, exactly the 0xe9 the error above names

This is why the fix is almost never "the file is corrupted" — it's a single, identifiable encoding mismatch, and Windows-1252 (or its close relative Latin-1) is the encoding behind the overwhelming majority of real-world cases, because it's what Excel and most Windows software write by default.

The Tempting Fix That Silently Corrupts Data

Don't reach for this as the fix, even though it makes the exception disappear:
df = pd.read_csv("data.csv", encoding="utf-8", encoding_errors="replace")
print(df)
#        name    city
# 0      Jos<0xEF><0xBF><0xBD>  Bogot<0xEF><0xBF><0xBD>
# 1  Fran<0xEF><0xBF><0xBD>ois  Z<0xEF><0xBF><0xBD>rich

No exception, but every byte pandas couldn't decode as UTF-8 was replaced with the Unicode replacement character (U+FFFD) and is now permanently gone — "José" became "Jos" plus a placeholder glyph, unrecoverable from this DataFrame. This is fine only if you've deliberately decided lossy data is acceptable; reached for as a quick fix for the exception itself, it's a silent data-corruption bug that looks like a successful read.

The Actual Fix

Pass the correct encoding directly. If you already know the file's source (Excel/Windows export), cp1252 or latin-1 resolves it immediately:

df = pd.read_csv("data.csv", encoding="cp1252")
# or, near-equivalent for this class of file:
df = pd.read_csv("data.csv", encoding="latin-1")

When the source isn't known, detect it instead of guessing — charset-normalizer (pip install charset-normalizer) inspects the file's actual byte statistics:

from charset_normalizer import from_path

result = from_path("data.csv").best()
df = pd.read_csv("data.csv", encoding=result.encoding)
Note: once you've read the file correctly, write it back out as UTF-8 (pandas' default for to_csv()) so the encoding mismatch doesn't propagate to every downstream consumer of the cleaned file.

Summary: Which Fix Applies

Situation                                                 Fix
─────────────────────────────────────────────────────    ──────────────────────────────────────
Known Excel/Windows-exported CSV                          encoding="cp1252" (or "latin-1")
Source/encoding unknown                                    charset-normalizer's from_path().best()
Need the error gone but data loss is unacceptable           never encoding_errors="replace"
Re-sharing the cleaned file downstream                      re-save with to_csv() (defaults to UTF-8)