Python Zipfile: Compress and Extract Archives

Python's built-in zipfile module provides a complete toolkit for manipulating ZIP archives directly within your applications without external dependencies. This article explains how zipfile processes data under the hood, how it implements compression and decompression algorithms through standard libraries, and how to programmatically create and unpack ZIP files using pure Python.

The Architecture of a ZIP File

To understand how zipfile operates, you must understand the structure of a standard ZIP archive. Unlike tarballs (.tar.gz), which compress an entire collection of files as a single stream, a ZIP file compresses each file individually and stores them alongside metadata headers.

A standard ZIP archive consists of:

  1. Local File Headers and Data: Each archived file has a preceding local header containing metadata (file name, uncompressed size, compressed size, timestamp) followed immediately by the compressed data block.
  2. Central Directory: Located toward the end of the archive, this table lists all contained files, their exact byte offsets, and storage attributes.
  3. End of Central Directory (EOCD) Record: The final bytes of the file, which mark the boundary and point to the beginning of the Central Directory.

Because the master index resides at the end in the Central Directory, the zipfile module can inspect, list, or selectively unpack individual files without decompressing the entire archive.

How zipfile Compresses Data

The zipfile module itself is written in pure Python, located in the standard library as zipfile.py. However, heavy mathematical operations for compression are delegated to built-in C-extensions that come pre-packaged with Python.

Compression Modes

When writing files to an archive, zipfile supports several compression algorithms specified via the compression argument:

Writing an Archive

When you add a file using .write() or .writestr(), zipfile executes the following sequence:

  1. Header Creation: It generates a local file header with a temporary CRC-32 checksum and size values.
  2. Streaming and Compressing: It reads the source file in chunks (typically 64 KB or custom buffer sizes) and feeds them into the chosen compressor object (such as zlib.compressobj).
  3. Writing Payload: The compressed stream is written to the archive target.
  4. Directory Staging: The file's metadata, byte position, and final CRC-32 checksum are appended to an in-memory Central Directory list.
  5. Finalization: When the ZipFile object closes, it writes the complete Central Directory and the EOCD record to the end of the file.
import zipfile

files_to_pack = ['document.txt', 'data.csv']

with zipfile.ZipFile('archive.zip', mode='w', compression=zipfile.ZIP_DEFLATED) as archive:
    for file in files_to_pack:
        archive.write(file)

To create files directly from memory without saving them to the disk first, use writestr():

with zipfile.ZipFile('archive.zip', mode='a', compression=zipfile.ZIP_DEFLATED) as archive:
    archive.writestr('virtual_file.txt', 'This data was generated in memory.')

How zipfile Extracts Data

Extraction is an inverse process optimized by the Central Directory index.

Reading and Validation

When zipfile.ZipFile opens an existing archive in read mode ('r'):

  1. It seeks to the end of the file to find the EOCD record.
  2. It parses the location of the Central Directory and reads the complete index into memory as ZipInfo objects.
  3. Because the directory is already mapped, operations like archive.namelist() or inspecting timestamps take minimal time and zero decompression overhead.

Decompressing Data

When extracting an item via .extract() or reading it via .open():

  1. zipfile checks the ZipInfo record for the file's exact byte offset in the archive.
  2. The file pointer seeks directly to the local data stream.
  3. It passes the raw compressed bytes through a decompressor object (such as zlib.decompressobj).
  4. As chunks decompress, the module calculates the running CRC-32 checksum.
  5. If the calculated checksum does not match the header's recorded checksum, a zipfile.BadZipFile exception is raised, ensuring file integrity.
import zipfile

# Extracting all files to a specific directory
with zipfile.ZipFile('archive.zip', mode='r') as archive:
    archive.extractall(path='extracted_files/')

# Extracting or reading a single file into memory
with zipfile.ZipFile('archive.zip', mode='r') as archive:
    with archive.open('document.txt') as file:
        content = file.read().decode('utf-8')
        print(content)

Security Considerations: Zip Slip

Historically, malicious ZIP files could contain relative paths (e.g., ../../etc/passwd) designed to write files outside the target directory during extraction—a vulnerability known as "Zip Slip."

Modern versions of Python mitigate this: