Working with ZIP Archives in Python: A Practical Deep Dive into zipfile

Read, write, append, and safely extract ZIP archives with Python's standard-library zipfile module — compression choices, ZipInfo metadata, in-memory archives, and the path-traversal pitfall that bites almost everyone.

The ZIP format is everywhere: software downloads, email attachments, exported data dumps, and the .docx/.xlsx files sitting on your desktop (which are ZIP archives under the hood). If you have ever double-clicked one to unpack it, you already know the end-user side of the story. For a friendly, non-programmer walkthrough of what a ZIP file actually is and how to open one on Windows, macOS, or a phone, the team at technik-frage.de wrote a solid explainer: ZIP-Datei öffnen und entpacken. This article picks up where that one leaves off — how to do all of it from Python, using the standard-library zipfile module, with no third-party dependencies.

Reading an archive

The entry point is the ZipFile object, used as a context manager so the file handle always closes cleanly:

import zipfile

with zipfile.ZipFile("data.zip") as zf:
    print(zf.namelist())          # every path inside the archive
    with zf.open("report.csv") as f:
        first_line = f.readline()  # stream a member without extracting

Notice that zf.open() hands you a file-like object. You can read a single member straight into memory without ever writing it to disk — ideal when you only need one file out of a large archive, or when you are running somewhere with no writable temp directory.

Writing and appending

Creating an archive uses the same class with a different mode. Pass a compression method explicitly, because the default (ZIP_STORED) does no compression at all:

with zipfile.ZipFile("out.zip", "w", zipfile.ZIP_DEFLATED) as zf:
    zf.write("report.csv")                 # add an existing file
    zf.writestr("notes.txt", "generated!")  # add bytes/str directly

Use mode "a" to append to an existing archive and "x" to fail loudly if the target already exists. writestr() is the underrated hero here: it lets you build an archive entirely from data you generated in the program, which pairs nicely with an in-memory buffer:

import io

buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
    zf.writestr("hello.txt", "no disk involved")
buffer.seek(0)  # now ready to send over a network response

Compression methods that matter

Beyond ZIP_DEFLATED, Python also exposes ZIP_BZIP2 and ZIP_LZMA, which trade CPU time for smaller output. For already-compressed payloads — JPEGs, MP4s, or nested ZIPs — stick with ZIP_STORED; running Deflate over incompressible bytes just wastes cycles. You can also set compresslevel per member on modern Python versions to tune the effort.

Metadata via ZipInfo

Every member carries a ZipInfo record with its name, timestamp, and sizes. Iterating over infolist() lets you inspect an archive without unpacking it:

with zipfile.ZipFile("data.zip") as zf:
    for info in zf.infolist():
        ratio = 1 - info.compress_size / max(info.file_size, 1)
        print(f"{info.filename}: {ratio:.0%} smaller")

The pitfall: extractall and path traversal

The single most important thing to know about zipfile is that extractall() is only partially safe. Historically, a maliciously crafted archive could contain member names like ../../etc/passwd — the so-called "Zip Slip" attack — and older code would happily write outside the target directory. Modern Python sanitizes absolute paths and leading slashes, but you should still never extract untrusted archives blindly. Validate members first:

import os

def safe_extract(zf, dest):
    dest = os.path.abspath(dest)
    for member in zf.namelist():
        target = os.path.abspath(os.path.join(dest, member))
        if not target.startswith(dest + os.sep):
            raise ValueError(f"unsafe path in archive: {member}")
    zf.extractall(dest)

Two more gotchas worth remembering: zipfile can read encrypted archives with zf.setpassword(b"secret") but it cannot create encrypted ones, and the legacy ZipCrypto scheme it decrypts is not secure. And for archives larger than 4 GB or with more than 65,535 members, pass allowZip64=True (the default on writes) so the extended format kicks in.

Wrapping up

With just the standard library you can inspect, stream, build, and safely unpack ZIP archives — no external packages required. If you ever need to explain the format to a non-technical colleague or want a refresher on the everyday side of ZIP files, that technik-frage.de guide to opening and extracting ZIP files is a good companion piece. From there, zipfile gives you the programmatic superpowers.