Reading a CSV that is not UTF-8
An encoding is the rule that turns the bytes in a file into characters, and encoding='latin-1' is the argument that lets pandas.read_csv read a CSV whose bytes are not valid UTF-8.
Last updated: 12 Oct, 2026 · pandas 3.0 · scikit-learn 1.9
Text columns and the str dtype ended the part on the pandas traps. The work starts here, with a restaurant table that arrives inside a zip and refuses to open. Two errors stand between the file and a DataFrame, and both of them are common enough that meeting them here beats meeting them on your own data.
Syntax: encoding names the rule for decoding the bytes, and a file object from a zip is read exactly like a path.
pd.read_csv(path_or_url, encoding='latin-1') # decode with latin-1 instead of UTF-8
with zipfile.ZipFile(...) as zf: # a member of a zip
with zf.open('inside.csv') as fh: # opened as a file object
pd.read_csv(fh, encoding='latin-1')Opening a zip that holds more than one file
The video reads zomato.csv after unpacking the archive by hand. The code here opens the archive in memory instead, so there is no download and no unzip step before the read. The encoding problem that follows is the same one.
read_csv unpacks a zip by itself when there is exactly one file inside it. This archive holds eight, so pandas cannot guess which one to read and says so.
import pandas as pd
ZIP = ("https://raw.githubusercontent.com/krishnaik06/"
"5-Days-Live-EDA-and-Feature-Engineering/main/Zomatodataset.zip")
pd.read_csv(ZIP)Traceback (most recent call last):
File "main.py", line 5, in <module>
pd.read_csv(ZIP)
ValueError: Multiple files found in ZIP file. Only one file per ZIP: ['Country-Code.xlsx', 'file1.json', 'file2.json', 'file3.json', 'file4.json', 'file5.json', 'Untitled.ipynb', 'zomato.csv']The error lists the members, which is already useful. To pick one of them, download the archive into memory and open it with the standard library.
Downloading the archive into memory
urllib.request.urlopen(ZIP).read() returns the bytes of the file. io.BytesIO wraps those bytes so they behave like an open file, and zipfile.ZipFile reads the archive out of it. Nothing is written to disk.
import io, zipfile, urllib.request
import pandas as pd
ZIP = ("https://raw.githubusercontent.com/krishnaik06/"
"5-Days-Live-EDA-and-Feature-Engineering/main/Zomatodataset.zip")
zf = zipfile.ZipFile(io.BytesIO(urllib.request.urlopen(ZIP).read()))Listing what is inside
namelist() gives the member names and getinfo(name).file_size gives the unpacked size of each, which is how you tell the table from the extras.
import io, zipfile, urllib.request
import pandas as pd
ZIP = ("https://raw.githubusercontent.com/krishnaik06/"
"5-Days-Live-EDA-and-Feature-Engineering/main/Zomatodataset.zip")
zf = zipfile.ZipFile(io.BytesIO(urllib.request.urlopen(ZIP).read()))
for name in zf.namelist():
print(f"{zf.getinfo(name).file_size:>9,} bytes {name}") 8,783 bytes Country-Code.xlsx
2,198,732 bytes file1.json
20,006,072 bytes file2.json
15,610,388 bytes file3.json
16,427,749 bytes file4.json
669,503 bytes file5.json
72 bytes Untitled.ipynb
2,257,316 bytes zomato.csvTwo members matter: zomato.csv is the table, and Country-Code.xlsx is a lookup that the merging lesson uses. The five JSON files hold the same restaurants in another format and are not needed.
The read that cannot decode
zf.open('zomato.csv') hands back a file object, and read_csv takes it like any other. The default encoding is UTF-8, and this file is not UTF-8.
with zf.open('zomato.csv') as fh:
pd.read_csv(fh)Traceback (most recent call last):
File "main.py", line 2, in <module>
pd.read_csv(fh)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xed in position 7044: invalid continuation byteThe message is precise: at byte 7044 there is 0xed, and in UTF-8 that byte has to be followed by a particular pattern of continuation bytes, which it is not. Byte 7044 falls inside a city name, Brasília, written with a single byte for the accented letter. UTF-8 would use two.
Choosing an encoding
There is no way to read an encoding out of a plain text file: it is not recorded anywhere, so it has to be tried. Three are worth trying, in this order.
for enc in ['utf-8', 'cp1252', 'latin-1']:
with zf.open('zomato.csv') as fh:
try:
frame = pd.read_csv(fh, encoding=enc)
print(f"{enc:8} ok {frame.shape} row 22 city {frame.loc[22, 'City']!r}")
except UnicodeDecodeError as err:
print(f"{enc:8} fails {err}")utf-8 fails 'utf-8' codec can't decode byte 0xed in position 7044: invalid continuation byte cp1252 fails 'charmap' codec can't decode byte 0x81 in position 9144: character maps to <undefined> latin-1 ok (9551, 21) row 22 city 'Brasí_lia'
Reading the three attempts
- utf-8 stops at byte 7044. It is the default and the right first guess, because most files written this decade are UTF-8.
- cp1252 gets further and stops at byte 9144. It is the Windows western European encoding, and it leaves five of the 256 byte values undefined.
0x81is one of them. - latin-1 reads the whole file and gives 9551 rows and 21 columns. It maps all 256 byte values to a character, so it never raises on any input.
- Row 22 reads
'Brasí_lia', not'Brasília'. latin-1 decoded the accented letter correctly and the stray underscore was in the file to begin with. An encoding that reads without error is not proof that every character came out right.
The frame the next lessons use
With the encoding settled, the read is one line. df is the Zomato table, and the rest of this part audits it.
with zf.open('zomato.csv') as fh:
df = pd.read_csv(fh, encoding='latin-1')
print(df.shape)
print(df[['Restaurant Name', 'City', 'Cuisines']].head(3).to_string())(9551, 21)
Restaurant Name City Cuisines
0 Le Petit Souffle Makati City French, Japanese, Desserts
1 Izakaya Kikufuji Makati City Japanese
2 Heat - Edsa Shangri-La Mandaluyong City Seafood, Asian, Filipino, Indianutf-8 vs cp1252 vs latin-1
| utf-8 | cp1252 | latin-1 | |
|---|---|---|---|
| Also called | UTF-8, the default | Windows-1252, ANSI | ISO-8859-1 |
| Bytes per character | 1 to 4 | 1 | 1 |
| Characters it covers | Every one | Western European plus quotes and dashes | Western European |
| Can it fail? | Yes, on an invalid byte sequence | Yes, on 5 undefined byte values | No, every byte maps to something |
| On this file | Stops at byte 7044 | Stops at byte 9144 | Reads all 9551 rows |
| Use it when | The file is modern or you wrote it | The file came out of Excel on Windows | No other encoding works and you can check the result |
Where an encoding argument is needed
- A file exported from an old system. Spreadsheets, banking extracts and survey tools often still write cp1252 or latin-1.
- Data with names and places in it. A table of English words decodes as UTF-8 by accident, because the first 128 byte values mean the same thing in all three encodings. The first accented name is where it breaks.
- A dataset downloaded from a competition or an archive. A zip of several files, one of which is the table, is the usual packaging.
encoding='latin-1' never raises, which makes it a tempting default and a quiet way to corrupt text. Always look at a column of names afterwards. If accented characters come out as pairs of odd symbols, the file was UTF-8 after all and latin-1 read each byte on its own.Related
- Previous: Text columns and the str dtype
- Next: The first look at a dataset
- See also: The datasets this course uses for the plain CSV and Excel reads
- Reference: pandas.read_csv and the standard encodings in the Python documentation
- Add
'utf-16'to the list of encodings and read the error it gives, which is a different one again. - Read the file with
encoding='utf-8', encoding_errors='replace'and printframe.loc[22, 'City']. What has replaced the accented letter? - Open
Country-Code.xlsxfrom the same archive withpd.read_exceland print its shape.
Little by little, you're building something great.