tarfile.ReadError: file could not be opened successfully (tested)
tarfile.ReadError: file could not be opened successfully lists four failed methods and hides the actual cause. We reproduced each cause: an empty file, an HTML page saved as .tar.gz, a wrong mode, a zip with a tar name, and truncated downloads — where the error you get is not a ReadError at all.
1. The exact error: empty file vs saved HTML page
import io, os, tarfile, tempfile
d = tempfile.mkdtemp()
src = os.path.join(d, "a.txt")
open(src, "w").write("hello\n" * 5000)
good = os.path.join(d, "good.tar.gz")
with tarfile.open(good, "w:gz") as t:
t.add(src, arcname="a.txt")
data = open(good, "rb").read()
cases = {
"empty file": b"",
"HTML 404 page saved as .tar.gz": b"<!DOCTYPE html><html><body>404 Not Found</body></html>",
"valid archive": data,
}
for label, blob in cases.items():
p = os.path.join(d, "f.tar.gz")
open(p, "wb").write(blob)
try:
with tarfile.open(p) as t:
print(f"{label}: OK {t.getnames()}")
except tarfile.ReadError as e:
print(f"{label}: ReadError: {e}")
print("-" * 40)
Output:
empty file: ReadError: file could not be opened successfully:
- method gz: ReadError('empty file')
- method bz2: ReadError('not a bzip2 file')
- method xz: ReadError('not an lzma file')
- method tar: ReadError('empty file')
----------------------------------------
HTML 404 page saved as .tar.gz: ReadError: file could not be opened successfully:
- method gz: ReadError('not a gzip file')
- method bz2: ReadError('not a bzip2 file')
- method xz: ReadError('not an lzma file')
- method tar: ReadError('truncated header')
----------------------------------------
valid archive: OK ['a.txt']
----------------------------------------
Measured on Python 3.13.5: with the default mode ("r", autodetect), tarfile.open() tries every compression method and, when all fail, raises one ReadError listing each attempt. Read the last line: method tar: ReadError('empty file') means a 0-byte file (a failed download, or a file still being written); 'truncated header' together with 'not a gzip file' means the bytes are not an archive at all — here an HTML 404 page saved under a .tar.gz name.
2. Explicit modes give a short message (and a zip renamed .tar.gz)
import os, tarfile, tempfile, zipfile
d = tempfile.mkdtemp()
src = os.path.join(d, "a.txt")
open(src, "w").write("hello\n" * 5000)
gz = os.path.join(d, "good.tar.gz")
with tarfile.open(gz, "w:gz") as t:
t.add(src, arcname="a.txt")
# Explicit mode: one short message instead of the list
for mode in ("r:gz", "r:bz2", "r:xz"):
try:
tarfile.open(gz, mode)
print(mode, "OK")
except tarfile.ReadError as e:
print(f"{mode}: ReadError: {e}")
# A .zip renamed to .tar.gz
z = os.path.join(d, "really_a_zip.tar.gz")
with zipfile.ZipFile(z, "w") as zf:
zf.write(src, "a.txt")
print("first bytes:", open(z, "rb").read(4))
try:
tarfile.open(z, "r:gz")
except tarfile.ReadError as e:
print("zip as r:gz: ReadError:", e)
print("is_tarfile:", tarfile.is_tarfile(z), "| is_zipfile:", zipfile.is_zipfile(z))
Output:
r:gz OK
r:bz2: ReadError: not a bzip2 file
r:xz: ReadError: not an lzma file
first bytes: b'PK\x03\x04'
zip as r:gz: ReadError: not a gzip file
is_tarfile: False | is_zipfile: True
Measured: with an explicit mode you get one short reason instead of the list: not a gzip file, not a bzip2 file, not an lzma file. A wrong mode on a valid archive (r:bz2 on a gzip file) fails the same way as a corrupt file, so prefer plain "r" unless you must enforce a format. A zip archive renamed to .tar.gz starts with PK\x03\x04; tarfile.is_tarfile() says False and zipfile.is_zipfile() says True, so open it with zipfile (see our BadZipFile guide).
3. The trap: a truncated .tar.gz is NOT a ReadError
import os, tarfile, tempfile
d = tempfile.mkdtemp()
src = os.path.join(d, "a.bin")
open(src, "wb").write(os.urandom(200_000))
gz = os.path.join(d, "good.tar.gz")
with tarfile.open(gz, "w:gz") as t:
t.add(src, arcname="a.bin")
data = open(gz, "rb").read()
cut = os.path.join(d, "cut.tar.gz")
open(cut, "wb").write(data[: len(data) // 2]) # download stopped halfway
print("is_tarfile:", tarfile.is_tarfile(cut))
try:
with tarfile.open(cut) as t:
t.extractall(os.path.join(d, "out"), filter="data")
except tarfile.ReadError as e:
print("caught ReadError:", e)
except Exception as e:
print(f"NOT a ReadError -> {type(e).__module__}.{type(e).__name__}: {e}")
print("subclass of tarfile.TarError?", isinstance(e, tarfile.TarError))
Output:
is_tarfile: True
NOT a ReadError -> builtins.EOFError: Compressed file ended before the end-of-stream marker was reached
subclass of tarfile.TarError? False
Measured: a download cut off halfway still starts with a valid gzip header, so tarfile.is_tarfile() returns True and open() succeeds. The failure comes during extraction as a built-in EOFError: Compressed file ended before the end-of-stream marker was reached, and EOFError is not a subclass of tarfile.TarError. Code that only does except tarfile.ReadError (or except tarfile.TarError) lets it crash the program, possibly after writing part of the files.
4. A truncated plain .tar: opens fine, fails later with 'unexpected end of data'
import os, tarfile, tempfile
d = tempfile.mkdtemp()
src = os.path.join(d, "a.bin")
open(src, "wb").write(os.urandom(200_000))
tar = os.path.join(d, "good.tar") # uncompressed
with tarfile.open(tar, "w") as t:
t.add(src, arcname="a.bin")
cut = os.path.join(d, "cut.tar")
open(cut, "wb").write(open(tar, "rb").read()[:100_000])
t = tarfile.open(cut) # opening works...
print("opened OK, is_tarfile:", tarfile.is_tarfile(cut))
try:
print(t.getnames()) # ...reading the members fails
except tarfile.ReadError as e:
print("ReadError:", e)
t.close()
Output:
opened OK, is_tarfile: True
ReadError: unexpected end of data
Measured: for an uncompressed .tar cut at 100 KB of 200 KB, open() and is_tarfile() both succeed; the ReadError: unexpected end of data only appears when tarfile walks the members (getnames(), getmembers(), extractall()). So wrap the whole read, not just the open() call.
5. A robust opener: check the first bytes, catch the right exceptions
import os, tarfile, tempfile
def open_archive(path):
with open(path, "rb") as f:
head = f.read(8)
if os.path.getsize(path) == 0:
raise ValueError(f"{path} is empty (failed download?)")
if head.lstrip()[:1] == b"<":
raise ValueError(f"{path} is HTML, not an archive: {head!r}")
if head[:4] == b"PK\x03\x04":
raise ValueError(f"{path} is a zip file; use zipfile")
return tarfile.open(path) # mode "r" = autodetect
def extract(path, dest):
try:
with open_archive(path) as t:
t.extractall(dest, filter="data")
return "ok"
except (tarfile.TarError, EOFError, OSError, ValueError) as e:
return f"{type(e).__name__}: {e}"
d = tempfile.mkdtemp()
src = os.path.join(d, "a.bin")
open(src, "wb").write(os.urandom(200_000))
plain = os.path.join(d, "plain_named.tar.gz") # uncompressed tar, .gz name
with tarfile.open(plain, "w") as t:
t.add(src, arcname="a.bin")
gz = os.path.join(d, "good.tar.gz")
with tarfile.open(gz, "w:gz") as t:
t.add(src, arcname="a.bin")
cut = os.path.join(d, "cut.tar.gz")
open(cut, "wb").write(open(gz, "rb").read()[:60_000])
html = os.path.join(d, "page.tar.gz")
open(html, "w").write("<!DOCTYPE html><title>404</title>")
for p in (gz, plain, cut, html):
print(f"{os.path.basename(p):18} {extract(p, os.path.join(d, 'out'))}")
try:
tarfile.open(plain, "r:gz")
except tarfile.ReadError as e:
print("plain tar with mode 'r:gz':", e)
Output:
good.tar.gz ok
plain_named.tar.gz ok
cut.tar.gz EOFError: Compressed file ended before the end-of-stream marker was reached
page.tar.gz ValueError: /tmp/tmpl6f1kx9j/page.tar.gz is HTML, not an archive: b'<!DOCTYP'
plain tar with mode 'r:gz': not a gzip file
Measured: sniffing the first bytes turns the confusing multi-line message into a clear one (empty, HTML, or zip), autodetect mode opens an uncompressed tar that is misnamed .tar.gz (an explicit "r:gz" rejects it with not a gzip file), and catching (tarfile.TarError, EOFError, OSError, ValueError) covers the truncated case. tarfile.ReadError subclasses TarError, which subclasses plain Exception. Extract into a fresh temporary directory and move it into place only on success, so a failed run doesn't leave half the files behind. filter="data" is the safe extraction filter; see our path-traversal guide.
6. If the file came from a download
- Check
r.status_codeandr.headers["Content-Type"]before saving:text/htmlmeans you are about to save an error or login page as your archive. - Compare the file size with the server's
Content-Length; a shorter file is the truncated case from section 3. See our IncompleteRead guide for connections cut mid-body. - For GitHub release or API tarball URLs, follow redirects (
requestsdoes by default;curlneeds-L), or you save a tiny redirect body instead of the archive.
Frequently asked questions
What does tarfile.ReadError: file could not be opened successfully mean?
tarfile tried gzip, bz2, xz and plain tar and all failed. We measured the last line: 'empty file' for a 0-byte file and 'truncated header' (with 'not a gzip file') for an HTML page saved as .tar.gz.
Why does except tarfile.ReadError not catch my truncated .tar.gz?
A truncated gzip stream raises the built-in EOFError ('Compressed file ended before the end-of-stream marker was reached'), which is not a TarError. We measured is_tarfile() returning True for that file and the error appearing during extraction.
What is tarfile.ReadError: not a gzip file?
You opened with mode 'r:gz' and the file is not gzip-compressed: a plain tar, a zip, or an HTML page. We measured all three; mode 'r' autodetects and opened the plain tar.
What does ReadError: unexpected end of data mean?
An uncompressed .tar was cut short. We measured open() and is_tarfile() succeeding and the error appearing only when reading the members, so re-download the file.
Can tarfile open a zip file?
No. We measured a zip renamed .tar.gz: is_tarfile() False, is_zipfile() True. Use the zipfile module.
Written with AI assistance. Measured on 2026-10-09 on Debian with Python 3.13.5's built-in tarfile: an empty file, an HTML page saved as .tar.gz, explicit r:gz/r:bz2/r:xz modes, a zip renamed .tar.gz, a .tar.gz and a plain .tar truncated mid-file, and a byte-sniffing opener. Every block was run verbatim and its output pasted from that run. PlainHeaders is our extension; Python's tarfile is built in.