Python Unzip Files With zipfile

Python can unzip files with the standard-library zipfile module. The usual workflow is to open a ZIP archive with ZipFile, inspect the members, choose an output directory, and extract only after the destination paths are checked.

The official references are the Python documentation for zipfile, pathlib, and tempfile.

A ZIP file is just a container of named members. Those names can include folders, and they can also come from outside your program. Treat member names as input, not as trusted local paths. Before extracting, resolve the final path under a known output directory and reject anything that would land elsewhere.

The examples below create ZIP files inside temporary directories before reading or extracting them. That keeps the examples safe to run, repeatable, and independent of files on your computer.

For day-to-day scripts, prefer pathlib.Path for path joins and checks. It keeps the archive path, output path, parent folders, and resolved safety checks in one readable style.

Unzip A File To A Folder

extractall() extracts every member from an archive. Check the resolved destination for every member first, then extract into a folder you created for that job.

from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZipFile

def is_safe_member(root, name):
    target = (root / name).resolve()
    return target == root or root in target.parents

with TemporaryDirectory() as folder:
    work = Path(folder)
    zip_path = work / "reports.zip"
    extract_to = work / "extracted"

    with ZipFile(zip_path, "w") as archive:
        archive.writestr("reports/january.txt", "sales\n")
        archive.writestr("reports/february.txt", "costs\n")

    extract_to.mkdir()
    with ZipFile(zip_path) as archive:
        root = extract_to.resolve()
        for name in archive.namelist():
            if not is_safe_member(root, name):
                raise ValueError(f"Unsafe ZIP member: {name}")
        archive.extractall(extract_to)

    extracted = sorted(
        path.relative_to(extract_to).as_posix()
        for path in extract_to.rglob("*")
        if path.is_file()
    )
    print(extracted)

The safety helper resolves the final destination before extraction. A normal member such as reports/january.txt stays under the output folder, so it is allowed.

This pattern is a practical default when the archive comes from a user upload, an email attachment, a build system, or a remote service. It is a few extra lines, but those lines make the extraction boundary explicit.

List ZIP Members Before Extracting

Sometimes you need to inspect an archive before writing anything to disk. infolist() returns metadata such as member names, compressed sizes, and original file sizes.

from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZipFile

with TemporaryDirectory() as folder:
    zip_path = Path(folder) / "dataset.zip"

    with ZipFile(zip_path, "w") as archive:
        archive.writestr("docs/readme.txt", "Read me\n")
        archive.writestr("data/counts.csv", "name,count\nred,3\n")

    with ZipFile(zip_path) as archive:
        for info in archive.infolist():
            member = Path(info.filename)
            if member.is_absolute() or ".." in member.parts:
                raise ValueError(f"Unsafe member name: {info.filename}")
            print(info.filename, info.file_size)

Listing first is useful for logging, validation, progress displays, and allow-list rules. You might accept only text files, only one top-level folder, or only names that match a known export format.

Do not use the member name as a local path without checking it. The name is archive data. It should become a filesystem path only after your code has decided that the resolved target belongs under the extraction root.

Reject Unsafe Archive Paths

A member name such as ../outside.txt should not be extracted. The safest response is to detect it before any member is written, then stop the extraction.

from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZipFile

def safe_zip_target(root, member_name):
    root = root.resolve()
    target = (root / member_name).resolve()
    if target != root and root not in target.parents:
        raise ValueError(f"Blocked ZIP member: {member_name}")
    return target

with TemporaryDirectory() as folder:
    work = Path(folder)
    zip_path = work / "mixed.zip"
    destination = work / "safe-output"
    destination.mkdir()

    with ZipFile(zip_path, "w") as archive:
        archive.writestr("good/data.txt", "ok\n")
        archive.writestr("../outside.txt", "no\n")

    try:
        with ZipFile(zip_path) as archive:
            for name in archive.namelist():
                safe_zip_target(destination, name)
            archive.extractall(destination)
    except ValueError as exc:
        print(exc)

    print((work / "outside.txt").exists())

The example deliberately creates a risky member name inside a temporary ZIP file, then refuses it. The final print shows that the outside path was not created.

Checking the whole archive before extraction is better than extracting member by member and discovering a problem halfway through. It leaves the output folder in a clearer state when validation fails.

Read A File Without Extracting

You do not always need to unzip everything. ZipFile.open() reads one member as a file-like object, which is useful for config files, manifests, small text files, or quick inspections.

from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZipFile

with TemporaryDirectory() as folder:
    zip_path = Path(folder) / "notes.zip"

    with ZipFile(zip_path, "w") as archive:
        archive.writestr("notes/todo.txt", "check paths\nextract later\n")

    member_name = "notes/todo.txt"
    member = Path(member_name)
    if member.is_absolute() or ".." in member.parts:
        raise ValueError(f"Unsafe member name: {member_name}")

    with ZipFile(zip_path) as archive:
        with archive.open(member_name) as handle:
            text = handle.read().decode("utf-8")

    print(text.splitlines()[0])

Reading directly avoids unnecessary files on disk. It also gives you a chance to inspect the archive contents before deciding whether extraction is needed at all.

When reading text from a ZIP member, decode bytes with the encoding your data uses. UTF-8 is a common default for modern text exports, but older systems may use another encoding.

Extract Only Selected Files

For larger archives, extract only the members your program needs. You can filter names, check paths, create parent folders, and copy bytes from the archive member to the final file.

from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZipFile

def safe_output_path(root, member_name):
    root = root.resolve()
    target = (root / member_name).resolve()
    if target != root and root not in target.parents:
        raise ValueError(f"Unsafe ZIP member: {member_name}")
    return target

with TemporaryDirectory() as folder:
    work = Path(folder)
    zip_path = work / "logs.zip"
    output = work / "selected"
    output.mkdir()

    with ZipFile(zip_path, "w") as archive:
        archive.writestr("logs/app.txt", "started\n")
        archive.writestr("logs/debug.tmp", "skip\n")
        archive.writestr("charts/summary.svg", "skip\n")

    with ZipFile(zip_path) as archive:
        for info in archive.infolist():
            if not info.filename.endswith(".txt"):
                continue
            target = safe_output_path(output, info.filename)
            target.parent.mkdir(parents=True, exist_ok=True)
            with archive.open(info) as source, target.open("wb") as target_file:
                target_file.write(source.read())

    print(sorted(path.name for path in output.rglob("*") if path.is_file()))

This approach is useful when a ZIP contains logs, generated artifacts, and metadata but your program needs only one category. It also makes the overwrite policy easy to place near the write step.

For very large files, stream in chunks instead of reading the whole member at once. The path check remains the same; only the copy loop changes.

Handle Bad ZIP Files

A file with a .zip suffix may still be corrupt or not be a ZIP archive at all. Use is_zipfile() for a quick check, and catch BadZipFile around the real open step.

from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import BadZipFile, ZipFile, is_zipfile

with TemporaryDirectory() as folder:
    bad_path = Path(folder) / "broken.zip"
    bad_path.write_text("not a zip archive\n", encoding="utf-8")

    print(is_zipfile(bad_path))
    try:
        with ZipFile(bad_path) as archive:
            print(archive.namelist())
    except BadZipFile:
        print("bad zip file")

Validation should happen before extraction, but it should not replace error handling. Between the check and the open call, the file might change, be truncated, or fail because of permissions.

The practical rule is to create a known output directory, inspect member names, resolve every target path under that directory, and extract only after those checks pass. Use ZipFile.open() when you only need to read one member, and catch BadZipFile when the archive might be invalid.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted