zipfile BadZipFile: Bad CRC-32 for file (tested)
zipfile.BadZipFile: Bad CRC-32 for file 'name' means one member's bytes changed after the zip was made. We corrupted real archives to measure what still works, what gets written before the error, and the zlib.error that slips past except BadZipFile.
1. The exact error: one damaged member, the rest fine
import os, tempfile, zipfile
d = tempfile.mkdtemp()
p = os.path.join(d, "data.zip")
with zipfile.ZipFile(p, "w", zipfile.ZIP_STORED) as z:
z.writestr("a.txt", "hello world\n" * 1000)
z.writestr("b.txt", "second file\n" * 1000)
raw = bytearray(open(p, "rb").read())
i = raw.find(b"hello world")
raw[i + 5000] ^= 0xFF # flip one byte inside a.txt's data
open(p, "wb").write(raw)
with zipfile.ZipFile(p) as z: # opening works: the directory is intact
print("namelist:", z.namelist())
print("testzip():", z.testzip()) # first bad member, or None
try:
z.read("a.txt")
except zipfile.BadZipFile as e:
print("read a.txt -> BadZipFile:", e)
print("read b.txt ->", len(z.read("b.txt")), "bytes OK")
Output:
namelist: ['a.txt', 'b.txt']
testzip(): a.txt
read a.txt -> BadZipFile: Bad CRC-32 for file 'a.txt'
read b.txt -> 12000 bytes OK
Measured on Python 3.13.5: flipping a single byte inside one member's data leaves the archive's directory intact, so ZipFile() opens it and namelist() works. Only reading that member raises zipfile.BadZipFile: Bad CRC-32 for file 'a.txt'. The other member reads normally. testzip() returns the name of the first bad member (and None when all are good), so one corrupt entry does not mean the whole zip is lost.
2. Compressed members: often zlib.error instead of BadZipFile
import collections, os, random, tempfile, zipfile
d = tempfile.mkdtemp()
p = os.path.join(d, "data.zip")
with zipfile.ZipFile(p, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("a.txt", random.Random(7).randbytes(50_000).hex())
good = open(p, "rb").read()
data_start, data_end = 30 + len("a.txt"), good.find(b"PK\x01\x02")
seen = collections.Counter()
for pos in range(data_start, data_end, 997): # flip one byte at many places
raw = bytearray(good)
raw[pos] ^= 0xFF
open(p, "wb").write(raw)
try:
with zipfile.ZipFile(p) as z:
z.read("a.txt")
seen["no error (!)"] += 1
except Exception as e:
seen[f"{type(e).__module__}.{type(e).__name__}: {str(e)[:62]}"] += 1
for k, v in seen.most_common():
print(f"{v:3} {k}")
Output:
47 zipfile.BadZipFile: Bad CRC-32 for file 'a.txt'
9 zlib.error: Error -3 while decompressing data: invalid distance too far ba
2 zlib.error: Error -3 while decompressing data: invalid literal/length code
Measured: we flipped one byte at 58 positions in a deflate-compressed member. 47 gave Bad CRC-32, but 11 raised zlib.error: Error -3 while decompressing data, which is not a BadZipFile subclass. So except zipfile.BadZipFile lets about one in six corruptions crash your program. Which one you get depends on where the damage lands: if the bytes still decompress, the CRC check catches it at the end; if they don't, zlib fails first.
3. extractall() leaves a partial file behind
import os, random, tempfile, zipfile
d = tempfile.mkdtemp()
p = os.path.join(d, "data.zip")
with zipfile.ZipFile(p, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("a.txt", random.Random(7).randbytes(50_000).hex())
raw = bytearray(open(p, "rb").read())
raw[len(raw) // 2] ^= 0xFF # corrupt the compressed stream
open(p, "wb").write(raw)
with zipfile.ZipFile(p) as z:
try:
z.read("a.txt")
except Exception as e:
print(f"{type(e).__module__}.{type(e).__name__}: {e}")
print("is BadZipFile?", isinstance(e, zipfile.BadZipFile))
try:
z.extractall(os.path.join(d, "out"))
except Exception as e:
print("extractall:", type(e).__name__)
left = os.path.join(d, "out", "a.txt")
print("partial file left behind:", os.path.exists(left),
os.path.getsize(left) if os.path.exists(left) else "")
Output:
zipfile.BadZipFile: Bad CRC-32 for file 'a.txt'
is BadZipFile? True
extractall: BadZipFile
partial file left behind: True 65536
Measured: extractall() raised BadZipFile but had already written 65,536 bytes of the 100,000-byte a.txt to disk. Anything that later picks up files from that folder sees a truncated file with no error. Extract to a temporary directory and move it into place only after extraction succeeds.
4. Streaming reads: the CRC is checked only at the end
import os, tempfile, zipfile
d = tempfile.mkdtemp()
p = os.path.join(d, "data.zip")
with zipfile.ZipFile(p, "w", zipfile.ZIP_STORED) as z:
z.writestr("rows.csv", "id,value\n" + "".join(f"{i},{i*i}\n" for i in range(5000)))
raw = bytearray(open(p, "rb").read())
raw[raw.find(b"4000,")] = ord("9") # corrupt one digit near the end
open(p, "wb").write(raw)
rows = 0
with zipfile.ZipFile(p) as z, z.open("rows.csv") as f:
try:
for line in f:
rows += 1
except zipfile.BadZipFile as e:
print(f"BadZipFile after {rows} rows already processed: {e}")
Output:
BadZipFile after 4798 rows already processed: Bad CRC-32 for file 'rows.csv'
Measured: a corrupted digit at row 4000 of a stored CSV went unnoticed while we iterated z.open(); BadZipFile came only near the end of the member, after 4,798 rows (including the corrupted one) had already been processed. If you stream rows into a database, wrap the load in a transaction you can roll back, or run a verification pass first (section 6).
5. Where the corruption comes from: text-mode saves and newline translation
import io, os, random, tempfile, zipfile
d = tempfile.mkdtemp()
p = os.path.join(d, "good.zip")
with zipfile.ZipFile(p, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("a.txt", random.Random(1).randbytes(30_000).hex())
good = open(p, "rb").read()
# 1) saved via text: bytes -> str -> bytes (like writing response.text to a file)
text_saved = good.decode("latin-1").encode("utf-8")
# 2) newline translation (FTP ASCII mode, or Windows text-mode copy)
crlf = good.replace(b"\n", b"\r\n")
for label, blob in [("good", good), ("saved as text (utf-8)", text_saved),
("LF -> CRLF translated", crlf)]:
try:
with zipfile.ZipFile(io.BytesIO(blob)) as z:
print(f"{label:22} testzip -> {z.testzip()}")
except Exception as e:
print(f"{label:22} {type(e).__name__}: {e}")
Output:
good testzip -> None
saved as text (utf-8) BadZipFile: Bad magic number for central directory
LF -> CRLF translated testzip -> a.txt
Measured: a zip that went through a bytes → str → bytes round trip (like open(p, "w").write(response.text)) fails before any CRC check with Bad magic number for central directory, because the shifted offsets break the directory. A zip whose \n bytes were turned into \r\n (FTP ASCII mode, a text-mode copy) still opens, but testzip() flags the member: that is the Bad CRC-32 case. Save downloads with "wb" and response.content or iter_content(), and transfer zips in binary mode. Other real causes: a flaky disk or USB stick, and a download that was resumed with mismatched ranges.
6. Verify a zip properly (catch zlib.error too)
import os, random, tempfile, zipfile, zlib
def verify_zip(path):
"""Return None if every member decompresses with a matching CRC, else the problem."""
try:
with zipfile.ZipFile(path) as z:
for info in z.infolist():
try:
with z.open(info) as f:
while f.read(1 << 20):
pass
except (zipfile.BadZipFile, zlib.error, EOFError) as e:
return f"{info.filename}: {type(e).__module__}.{type(e).__name__}: {e}"
except zipfile.BadZipFile as e:
return f"archive: {e}"
return None
d = tempfile.mkdtemp()
p = os.path.join(d, "data.zip")
with zipfile.ZipFile(p, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("a.txt", random.Random(7).randbytes(50_000).hex())
good = open(p, "rb").read()
for pos in (100, 10_065): # one zlib.error case, one CRC case
raw = bytearray(good)
raw[pos] ^= 0xFF
open(p, "wb").write(raw)
with zipfile.ZipFile(p) as z:
try:
print(f"pos {pos}: testzip() -> {z.testzip()}")
except Exception as e:
print(f"pos {pos}: testzip() raised {type(e).__module__}.{type(e).__name__}")
print(f"pos {pos}: verify_zip() -> {verify_zip(p)}")
Output:
pos 100: testzip() raised zlib.error
pos 100: verify_zip() -> a.txt: zlib.error: Error -3 while decompressing data: invalid distance too far back
pos 10065: testzip() -> a.txt
pos 10065: verify_zip() -> a.txt: zipfile.BadZipFile: Bad CRC-32 for file 'a.txt'
Measured: testzip() returned the bad name for the CRC case but raised zlib.error for the other, so it is not a safe check on its own. Reading every member in chunks and catching (zipfile.BadZipFile, zlib.error, EOFError) reports both cases as a value you can log. When a member is bad, the fix is a fresh copy of the file: re-download it and compare its size or published checksum. There is no option to skip the CRC check and the data would be wrong anyway. For zips that won't open at all (File is not a zip file), see our BadZipFile guide.
Frequently asked questions
What does BadZipFile: Bad CRC-32 for file mean?
The member's data doesn't match the checksum stored in the zip, so it was damaged after the zip was made. We measured one flipped byte causing it for one member while the other members still read fine.
How do I fix Bad CRC-32 in Python zipfile?
Get a fresh copy of the file (re-download in binary mode and compare sizes or checksums). The data is wrong, so there's no flag to ignore it. The other members can still be read; testzip() names the first bad one.
Why does my corrupt zip raise zlib.error instead of BadZipFile?
If the damaged deflate data can't be decompressed, zlib fails before the CRC check. We measured 11 of 58 single-byte corruptions raising zlib.error, which except zipfile.BadZipFile does not catch.
Does zipfile.testzip() catch all corruption?
No. We measured it returning the bad member name for a CRC mismatch but raising zlib.error for a different corruption of the same file.
Can a file saved in text mode cause Bad CRC-32?
Yes. We measured LF to CRLF translation producing a member that fails testzip(), and a bytes-to-str-to-bytes round trip producing 'Bad magic number for central directory'. Always write zips with 'wb'.
Written with AI assistance. Measured on 2026-10-09 on Debian with Python 3.13.5's built-in zipfile: single-byte corruption of stored and deflated members (58 positions), extractall() partial output, streaming reads, text-mode and CRLF-translated copies, and a verification function. Every block was run verbatim and its output pasted from that run. PlainHeaders is our extension; Python's zipfile is built in.