Migrating from zipfile, tarfile, shutil, and patool
Recipes for replacing the tool you're using now. Each section shows the old code, the Archivey equivalent, and — where it matters — what actually changes in behaviour.
The recurring theme: the stdlib modules are per-format and extract permissively by default; Archivey is one interface for every format and blocks unsafe members unless you opt out. Most migrations are shorter code plus stricter defaults.
Cheat sheet
| You're doing | Now | With Archivey |
|---|---|---|
| Open an archive | zipfile.ZipFile(p) / tarfile.open(p) |
archivey.open_archive(p) |
| List names | zf.namelist() / tf.getnames() |
[m.name for m in reader] |
| Member metadata | ZipInfo / TarInfo |
ArchiveMember (same shape for every format) |
| Read one member | zf.read(n) / tf.extractfile(n).read() |
reader.read(n) |
| Extract everything | zf.extractall(d) / tf.extractall(d, filter="data") |
archivey.extract(p, d) |
| Unpack any format | shutil.unpack_archive(p, d) |
archivey.extract(p, d) |
Decompress a .gz |
gzip.open(p) |
archivey.open_stream(p) |
| Detect the format | guess from the extension | archivey.detect_format(p) |
From zipfile
# Before
import zipfile
with zipfile.ZipFile("photos.zip") as zf:
for name in zf.namelist():
print(name, zf.getinfo(name).file_size)
data = zf.read("subdir/a.txt")
zf.extractall("out/")
# After
import archivey
with archivey.open_archive("photos.zip") as reader:
for member in reader:
print(member.name, member.size)
data = reader.read("subdir/a.txt")
reader.extract_all("out/")
What changes:
extractallwas never safe.zipfile.extractallsanitizes absolute paths and..by mangling them, but happily writes symlinks that point outside the destination.extract_allblocks traversal and symlink escapes by default and reports them asExtractionStatus.BLOCKED. See Safe extraction.- Passwords are an open-time argument, not per-call
pwd=:open_archive("secret.zip", password="hunter2"). Archivey also reads WinZip AES members (with the[recommended]extra), whichzipfilecannot decrypt at all. - Duplicate names are visible.
namelist()returns duplicates with no way to tell which one wins; Archivey marks the live entry withmember.is_current. See duplicate names. reader.read(name)needs nogetinforound-trip, and works the same on every format.
From tarfile
# Before
import tarfile
with tarfile.open("backup.tar.gz") as tf:
for info in tf:
print(info.name, info.size)
with tf.extractfile("etc/config") as f:
data = f.read()
tf.extractall("out/", filter="data")
# After
import archivey
with archivey.open_archive("backup.tar.gz") as reader:
for member in reader:
print(member.name, member.size)
data = reader.read("etc/config")
reader.extract_all("out/")
What changes:
- You no longer choose a filter.
tarfile'sfilter="data"(the 3.14 default, and aDeprecationWarningbefore that) is closest to Archivey's defaultExtractionPolicy.STRICT. Archivey's policies areSTRICT/STANDARD/TRUSTEDand apply to every format, not just tar. extractfilecan returnNonefor non-regular members, so stdlib code needs aNonecheck;reader.read(name)raises a typed error instead.- Compressed tars are solid.
tarfilelets you callextractfilein any order and silently re-decompresses from the start each time — that's the classic accidental O(n²). Archivey makes the cost visible viareader.costand steers you tostream_members(). See Access costs. - Truncated archives.
tarfileoften stops silently at a short read. Archivey gives you the recovered prefix and the error viamembers_report().
From shutil.unpack_archive
# Before
import shutil
shutil.unpack_archive("bundle.tar.xz", "out/")
# After
import archivey
archivey.extract("bundle.tar.xz", "out/")
What changes:
- More formats.
unpack_archivehandles zip/tar family only. Archivey adds RAR, 7z, ISO, and single-file streams through the same call. - Format comes from content, not the filename.
unpack_archivedispatches on the extension and fails on a misnamed file; Archivey sniffs the bytes. - Safe by default, with the same policy knobs as
extract_all. - You get an
ExtractionReportback — what was written, skipped, or blocked — instead ofNone.
From patool / shelling out to 7z / unrar
# Before
subprocess.run(["7z", "x", "-o" + dest, "archive.7z"], check=True)
# After
import archivey
archivey.extract("archive.7z", dest)
What changes:
- No external binary for 7z, and no CLI output parsing. Archivey has a native 7z
reader (common codecs in the core; PPMd/Deflate64 and AES via
[recommended]). - RAR still needs RARLAB
unrarorrarfor member data — metadata and listing are native. That is a licensing constraint, not an oversight: the RAR decompression algorithm may not be reimplemented, so a RARLAB binary stays in the picture for data while metadata does not need it. - Errors are exceptions, not exit codes, and hostile archives can't reach a shell: the whole point is not handing untrusted filenames to a subprocess.
- Wrong passwords raise
EncryptionErrorrather than prompting on a tty and hanging.
From py7zr / rarfile
Both remain useful as writers and as cross-check oracles; Archivey does not write 7z or RAR. For reading:
# Before
import py7zr
with py7zr.SevenZipFile("a.7z") as z:
z.extractall("out/")
# After
import archivey
archivey.extract("a.7z", "out/")
The reason to switch is memory safety and uniformity: Archivey parses 7z and RAR metadata
in pure Python rather than delegating to a third-party parser, and the same reader
interface covers every other format you handle. One RAR difference: rarfile will use
unar or 7z for member data if that is what is on PATH; archivey requires RARLAB
unrar or rar and will not fall back to those lookalikes.
Things that will bite you
Worth reading before you migrate a production path:
- Extraction is strict. Archives that "worked" with
extractallmay now reportBLOCKEDmembers. That is the point — but check theExtractionReportrather than assuming success. - One live member stream by default. If you held several
extractfile()handles open, passconcurrent_members=True. See supported platforms and threading. read()is all-or-raise. A truncated member raises instead of returning a short body; use a chunked loop if you want the recoverable prefix (reading members).- Random access on a pipe fails loudly instead of silently buffering the whole thing
into memory — an unbounded allocation you did not ask for is worse than an error.
Pass
streaming=Truefor a forward-only pass where the format has one; ZIP, ISO, 7z and RAR need seek in either mode, and the error says so instead of proposing a retry. - Salvage is not implemented yet. Reading a badly damaged archive gives you the recoverable prefix plus an honest error, not a best-effort resync past the damage.