Python zipfile

PYTHON Published Updated 4 mins read By Leon Wei
Python zipfile cover image

Quick summary

Summarize this blog with AI

Python's standard-library zipfile module can create, inspect, and read ZIP archives. It cannot create encrypted ZIP files. Calling setpassword while writing does not change that: it sets a default password for reading supported encrypted members. This guide demonstrates that distinction, then extracts a small, explicitly defined archive safely enough for a controlled tutorial workflow.

Examples use Python 3.12.14. They write only inside temporary directories. The extraction helper is intentionally limited to a known two-file bundle; it is not a general untrusted-archive upload service.

Create an archive and inspect its members

from io import BytesIO
from zipfile import ZipFile, ZIP_DEFLATED

buffer = BytesIO()
with ZipFile(buffer, "w", compression=ZIP_DEFLATED) as archive:
    archive.writestr("data.csv", "item,units\na,2\nb,3\n")
    archive.writestr("README.txt", "Fictional inventory\n")
bundle = buffer.getvalue()
with ZipFile(BytesIO(bundle)) as archive:
    print(archive.namelist())
    print(archive.read("data.csv").decode("utf-8").splitlines())

Expected output:

['data.csv', 'README.txt']
['item,units', 'a,2', 'b,3']

writestr writes supplied bytes or UTF-8-encoded text. To add an existing file, write accepts a filesystem path and a separate arcname; use an intentional archive name rather than leaking a machine's directory layout. Mode w replaces an existing archive, x refuses an existing destination, and a appends members. Appending a member with an existing name does not replace the earlier member cleanly; duplicate names require an explicit policy.

Prove that setpassword does not encrypt output

plain = BytesIO()
with ZipFile(plain, "w") as archive:
    archive.setpassword(b"demonstration-only")
    archive.writestr("note.txt", "still plaintext")
with ZipFile(BytesIO(plain.getvalue())) as archive:
    member = archive.getinfo("note.txt")
    print(bool(member.flag_bits & 1))
    print(archive.read("note.txt").decode("utf-8"))

Expected output:

False
still plaintext

The encryption flag is false and the content is readable without a password. Do not put confidential data into an archive under the assumption that this password protects it. ZIP compression is not encryption. If encrypted delivery is required, select a tool and format that actually support it, verify the receiving tool's compatibility, and handle keys separately. This tutorial deliberately does not claim AES support from zipfile.

Our consumer accepts exactly data.csv and README.txt, both ordinary flat files, with no duplicate names. The compressed input is capped at 1 MiB, each expanded member at 64 KiB, and total expanded content at 96 KiB. Only stored or deflated, unencrypted members are accepted. A fresh private temporary directory avoids overwriting existing user files or traversing an existing symlink tree.

from contextlib import contextmanager
from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZIP_STORED
import stat

@contextmanager
def open_bundle(blob):
    if not isinstance(blob, bytes):
        raise TypeError("bundle must be bytes")
    if len(blob) > 1024 * 1024:
        raise ValueError("compressed input is too large")
    expected = {"data.csv", "README.txt"}
    with ZipFile(BytesIO(blob)) as archive:
        members = archive.infolist()
        names = [info.filename for info in members]
        if len(members) != 2 or set(names) != expected:
            raise ValueError("expected two unique, known filenames")
        for info in members:
            mode = info.external_attr >> 16
            kind = stat.S_IFMT(mode)
            if info.is_dir() or kind not in (0, stat.S_IFREG):
                raise ValueError("only ordinary files are accepted")
            if info.flag_bits & 1:
                raise ValueError("encrypted members are not accepted")
            if info.compress_type not in (ZIP_STORED, ZIP_DEFLATED):
                raise ValueError("unsupported compression method")
            if info.file_size > 65536:
                raise ValueError("member is too large")
        if sum(info.file_size for info in members) > 98304:
            raise ValueError("expanded total is too large")
        with TemporaryDirectory(prefix="zip-demo-") as directory:
            root = Path(directory)
            total = 0
            for info in members:
                count = 0
                with archive.open(info) as source, (root / info.filename).open("xb") as target:
                    while chunk := source.read(4096):
                        count += len(chunk)
                        total += len(chunk)
                        if count > 65536 or total > 98304:
                            raise ValueError("expanded byte limit exceeded")
                        target.write(chunk)
                if count != info.file_size:
                    raise ValueError("member length mismatch")
            yield root

with open_bundle(bundle) as folder:
    print((folder / "data.csv").read_text(encoding="utf-8").splitlines())
    temporary_folder = folder
print(temporary_folder.exists())

Expected output:

['item,units', 'a,2', 'b,3']
False

All metadata is checked before any file is written. The name allowlist excludes absolute paths, dot-dot traversal, nested directories, and Windows drive syntax without trying to sanitize arbitrary filenames. Checking actual copied bytes supplements declared-size checks. Reading through each member verifies the ZIP CRC; a CRC is an integrity check, not proof of a trustworthy sender.

The returned folder is usable only inside the with block. Failure while copying or in the caller removes the temporary directory. The policy assumes another local process is not maliciously modifying this process's private directory. Byte limits do not establish a strict CPU-time budget; an internet-facing service also needs process isolation, resource limits, and an operational threat model.

Reject malformed and out-of-contract input explicitly

from zipfile import BadZipFile

wrong = BytesIO()
with ZipFile(wrong, "w") as archive:
    archive.writestr("../outside.txt", "no")
    archive.writestr("README.txt", "no")
for candidate in [b"not a zip", wrong.getvalue()]:
    try:
        with open_bundle(candidate):
            print("unexpected success")
    except (BadZipFile, ValueError) as exc:
        print(type(exc).__name__)

Expected output:

BadZipFile
ValueError

Catch exceptions at the boundary where you can report a failed import. Do not turn a failed extraction into a success message or return a partially populated folder. A trusted archive you created yourself may need only ordinary read operations; a user-uploaded archive needs a deliberate contract. Calling extractall on every received ZIP is not such a contract.

References: zipfile capabilities and extraction warnings, TemporaryDirectory cleanup, and file-type mode checks. For acquiring a file before archive inspection, see the bounded-download guide.

Interview Prep

Begin Your SQL, Python, and R Journey

Master 230 interview-style coding questions and build the data skills needed for analyst, scientist, and engineering roles.

Related Articles

All Articles
Python zipfile cover image
python Oct 17, 2026

Python zipfile

Learn how to create, read, extract, and modify ZIP archives with practical examples, by using this built-in standard library tool Python's zipfi…

Python requests cover image
python Oct 17, 2026

Python requests

Uncover the power of Python Requests for web communication. Learn HTTP essentials and harness the library for data retrieval, web scraping, and …

Python pyqt gui calculator cover image
python Apr 29, 2024

Python pyqt gui calculator

Embark on GUI programming journey with PyQt in Python Discover how to create intuitive cross-platform applications from simple windows to fully-…

Python main function cover image
python Apr 29, 2024

Python main function

Uncover the best practices for Python's main function, including handling, logging, and debugging. Learn to structure code for clarity and robus…