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.
Define an extraction contract before writing files
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.