Python zipfile: Safe ZIP Archives

Published on: July 27, 2026
Reading time: 4 minutes
ZIP archive icon for a Python zipfile article

The Python zipfile module is part of the standard library and provides tools to create, read, inspect, and extract ZIP archives without third-party packages. It is useful for backups, report exports, document delivery, upload processing, dataset packaging, and automation that needs to bundle many files into one portable container.

The API is compact, but a dependable production workflow must still handle internal names, total size, duplicate entries, integrity, and extraction paths. This guide explains how to use ZipFile, choose opening modes, write in-memory content, read files without extracting everything, inspect metadata, and apply practical validation. The subject also complements other standard-library topics such as graphlib for dependencies, contextvars for safe context, heapq priority queues, and singledispatch for extensible APIs.

Opening a ZIP archive

The main class is zipfile.ZipFile. Mode r opens an existing archive for reading, w creates or replaces one, a appends content, and x creates a new archive while failing if the destination already exists.

from zipfile import ZipFile

with ZipFile("data.zip", "r") as archive:
    print(archive.namelist())

Always prefer a context manager. ZIP files maintain a central directory containing member metadata, and an archive that is not closed correctly may remain incomplete.

Creating an archive

Use write() to add a file from disk. The arcname argument determines the internal name stored in the archive.

from zipfile import ZIP_DEFLATED, ZipFile

with ZipFile("backup.zip", "w", compression=ZIP_DEFLATED) as archive:
    archive.write("reports/sales.csv", arcname="sales.csv")
    archive.write("config/app.json", arcname="config/app.json")

Choose concise, portable internal names. Avoid absolute paths and unnecessary server-specific directories because they make archives harder to move and may reveal local structure.

Writing in-memory content

When data already exists as text or bytes, writestr() avoids creating a temporary file.

from zipfile import ZIP_DEFLATED, ZipFile

content = "id,name\n1,Ana\n2,Caio\n"
with ZipFile("export.zip", "w", ZIP_DEFLATED) as archive:
    archive.writestr("customers.csv", content)

This works well for small reports generated on demand. For very large outputs, use temporary files or chunked processing so that the entire result does not remain in memory.

Reading without extraction

The read() method returns a member as bytes. The open() method returns a file-like object and supports incremental processing.

from zipfile import ZipFile

with ZipFile("data.zip") as archive:
    with archive.open("config.json") as source:
        text = source.read().decode("utf-8")
        print(text)

This is useful when passing content directly to a JSON, CSV, XML, or text parser without writing another copy to disk.

Inspecting archive members

infolist() returns ZipInfo objects containing the member name, original size, compressed size, timestamp, and compression method.

with ZipFile("upload.zip") as archive:
    for item in archive.infolist():
        print(item.filename, item.file_size, item.compress_size)

Inspect metadata before extraction. Applications can reject unsupported extensions, too many entries, oversized members, or suspicious compression ratios.

Validating extraction paths

A member name should be treated as external input. Resolve the final destination and confirm that it remains under the approved root directory.

from pathlib import Path
from zipfile import ZipFile

root = Path("received").resolve()
with ZipFile("upload.zip") as archive:
    for item in archive.infolist():
        destination = (root / item.filename).resolve()
        if root not in destination.parents and destination != root:
            raise ValueError(f"Invalid archive path: {item.filename}")
        archive.extract(item, root)

Also reject absolute names, unnecessary parent components, and file types that your application does not need.

Applying resource limits

A small compressed file can expand into a much larger amount of data. Define a maximum per member and a maximum for the complete archive.

MAX_FILE = 50 * 1024 * 1024
MAX_TOTAL = 200 * 1024 * 1024

total = 0
for item in archive.infolist():
    if item.file_size > MAX_FILE:
        raise ValueError("Archive member is too large")
    total += item.file_size
    if total > MAX_TOTAL:
        raise ValueError("Archive exceeds the total limit")

Count the bytes actually copied as well. This allows the application to stop if the declared metadata does not match the stream being processed.

Checking integrity

testzip() reads archive members and checks their CRC. It returns the first problematic filename or None when no issue is found.

from zipfile import BadZipFile, ZipFile

try:
    with ZipFile("archive.zip") as archive:
        damaged = archive.testzip()
        if damaged:
            raise ValueError(f"Corrupted member: {damaged}")
except BadZipFile:
    print("The file is not a valid ZIP archive")

In an API, log technical details internally while returning a simple error message to the client.

Choosing a compression method

ZIP_DEFLATED is broadly compatible and suitable for most text and structured data. BZIP2 and LZMA may compress some content more effectively, but older tools may not support them. JPEG images, videos, and already compressed files usually show little improvement.

The compresslevel parameter controls the trade-off between speed and size. Measure actual data before increasing the level in high-volume jobs.

Handling duplicate names

An archive can contain multiple entries with the same name. Different tools may choose different entries, so normalize and track names before extraction.

seen = set()
for item in archive.infolist():
    name = item.filename.replace("\\", "/").casefold()
    if name in seen:
        raise ValueError(f"Duplicate archive name: {item.filename}")
    seen.add(name)

Consider case differences, slash styles, and equivalent Unicode representations when archives move between operating systems.

Controlled extraction

For external archives, inspect and copy one member at a time. This makes it possible to enforce limits, report progress, and remove partial files after a failure.

import shutil

for item in archive.infolist():
    destination = root / item.filename
    if item.is_dir():
        destination.mkdir(parents=True, exist_ok=True)
        continue
    destination.parent.mkdir(parents=True, exist_ok=True)
    with archive.open(item) as source, destination.open("wb") as target:
        shutil.copyfileobj(source, target, length=1024 * 1024)

Password-protected archives

The standard library can read some traditionally encrypted ZIP files, but it is not the right choice for modern confidentiality requirements. Sensitive information should use current encryption tools, sound key management, and an appropriate threat model.

Testing the workflow

Include tests for empty archives, missing files, invalid ZIP data, duplicate names, directories, Unicode filenames, large members, total-size limits, and invalid destination paths. Verify that partial outputs are removed when an exception occurs.

  • Use with for every archive operation.
  • Set deliberate internal names with arcname.
  • Validate final paths before writing files.
  • Limit member count, individual size, and total size.
  • Reject duplicate names and unsupported extensions.
  • Check integrity when correctness matters.
  • Extract into an isolated temporary directory.
  • Never execute extracted files automatically.
  • Log errors and remove incomplete results.
  • Test both normal and malformed archives.

Conclusion

Python zipfile covers the most common requirements for building and consuming ZIP archives. A production implementation should combine the basic API with metadata inspection, resource limits, path validation, and controlled extraction.

By treating every archive member as external input and verifying the destination before writing, applications become more predictable and resilient. Consult the official zipfile documentation and the OWASP guidance on safe extraction for additional details.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Asynchronous Python code representing asyncio.eager_task_factory
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    asyncio.eager_task_factory: Reduce Task Overhead

    Learn Python asyncio.eager_task_factory to reduce scheduling overhead, understand ordering changes, and optimize short coroutines safely.

    Ler mais

    Tempo de leitura: 4 minutos
    14/09/2026
    Developer working with UTC timestamps and Python calendar.timegm
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    calendar.timegm: Convert UTC to Unix Timestamps

    Learn Python calendar.timegm to convert UTC date tuples into Unix timestamps safely and avoid local-time conversion bugs.

    Ler mais

    Tempo de leitura: 5 minutos
    14/09/2026
    Programmer analyzing code to identify file MIME types with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    mimetypes.guess_file_type: Detect MIME Types

    Learn Python mimetypes.guess_file_type for MIME detection in paths, URLs, uploads, and HTTP responses with safe fallbacks.

    Ler mais

    Tempo de leitura: 5 minutos
    13/09/2026
    Server components representing isolated Python interpreters running in parallel
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    InterpreterPoolExecutor: True Parallelism in Python

    Learn Python InterpreterPoolExecutor for CPU-bound tasks, isolated interpreters, true parallelism, and safer concurrency design.

    Ler mais

    Tempo de leitura: 6 minutos
    13/09/2026
    Microprocessor representing CPUs available to a Python process
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    os.process_cpu_count: Count Available CPUs

    Learn Python os.process_cpu_count to size workers according to the CPUs actually available to the process.

    Ler mais

    Tempo de leitura: 4 minutos
    12/09/2026
    Laptop with digital code representing SQLite BLOB data
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    sqlite3.Blob: Incremental BLOB Reads and Writes

    Learn Python sqlite3.Blob for incremental BLOB reads and writes, lower memory use, and safer binary data handling in SQLite.

    Ler mais

    Tempo de leitura: 5 minutos
    12/09/2026