Python zipfile extract: unpack selected files safely
Inspect ZIP members, allow only expected text files, and extract them into one chosen directory.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Python zipfile extract: unpack selected files safely
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
Imagine a downloaded ZIP that promises one text note. Its member list also contains another file, and you need to decide what should land on disk. Predict the result before the first run. We will start with the convenient library API, inspect what it does, then give the application its own narrow acceptance policy. Notice that the lesson is about extracting rather than creating a ZIP; if you need to build an archive, the linked earlier tutorial covers that separately. The search for python zipfile extract usually begins with one line: ZipFile.extractall(destination). That line is useful for a trusted archive, but it extracts every member.
- 00:43
This lesson answers the practical follow- up: how do you unpack only expected text files from a ZIP you did not create? We inspect each ZipInfo entry, reject unsafe names, bound uncompressed size, and then write selected bytes under a chosen directory. The browser exercise uses an in-memory ZIP and its temporary filesystem; no uploaded archive is needed. Open a ZIP with a context manager and print its member names before writing files. The archive can contain more than the one document you expected. A member list is evidence about filenames, not evidence that the contents are safe.
- 01:23
Do this first even when a download has a friendly name such as notes.zip. The first run is intentionally broad. It succeeds, which is precisely why it can be easy to miss the bug: the program extracted everything, including a CSV unrelated to our text-note import. This is not a claim that Python extractall ignores path safety. The standard library normalizes some path forms, but it cannot know your application expects only top-level text files. We need to make that rule explicit rather than assume a general extraction helper enforces it. Start by viewing archive.namelist() rather than assuming the contents.
- 02:05
A ZIP may include directories, duplicate member names, nested paths, absolute- looking names, files you did not expect, and very large expanded content. Python's extractall performs some path normalization, but an application still needs its own acceptance policy. Here the policy is deliberately narrow: one top-level .txt filename, no slash or backslash, no dot-only name, no duplicate, and at most 4096 uncompressed bytes per file. This is a teaching policy, not a universal ZIP security limit. ZipInfo offers more detail than namelist: filename, file_size, compress_size, and directory status.
- 02:48
The extraction policy in this lesson accepts only one-level .txt names. That deliberate restriction also means a valid archive with nested text files will be rejected. If your application needs directories, define and test a different policy rather than silently weakening this one. Now inspect the archive without writing. For each ZipInfo object, read the member name and its declared expanded size. Consider how you would treat a directory, a duplicate name, an absolute-looking name, or a nested path. This example has only two tiny entries, but the same inspection pattern works on a larger archive.
- 03:29
The numbers are not proof of trusted content; they are inputs to your resource policy and allow you to reject some obviously unwanted members early. Use ZipFile.infolist() so that each member's name and file_size are available before writing. Reject the archive first if any member violates the contract; do not leave a partially extracted directory when a later entry is bad. Then create the destination, read each accepted member with archive.open(info), decode UTF-8, and write it with pathlib.Path.write_text. Keeping a list of validated ZipInfo objects matters when names are duplicated: reading by name alone can be ambiguous.
- 04:12
Close the archive with a with block even when validation raises. Checking a filename after extracting it defeats the point of an allowlist. Validate every member before the first write. Reject empty names, names with a slash or backslash, names that are not .txt, directories, duplicates, and entries above the size limit. Compare file_size with a limit suitable for your application, not a number copied blindly from a tutorial. The corrected function validates the complete list first. It allows one-level .txt filenames up to a chosen size, rejects duplicates and path separators, then reads through the validated ZipInfo objects and writes UTF-8
- 04:52
text. Keeping the exact ZipInfo instance avoids ambiguity if someone tried to put the same name into the archive twice. A temporary directory makes the demonstration repeatable and ensures no permanent file is left on this machine. The final check builds two small ZIPs in memory. One contains notes.txt and can be extracted to a temporary folder. The other contains ../outside.txt and is rejected before any write. A second policy check rejects a file whose advertised expanded size is above the limit.
- 05:32
For real untrusted archives also consider total expanded size, compression ratio, disk capacity, symlinks, and resource limits; never assume extension alone proves that content is harmless. The earlier zipfile lesson creates archives, while this guide is specifically about inspecting and extracting their members. A ZIP can contain repeated names. ZipFile.read('name') may select an ambiguous entry, so keep the validated ZipInfo objects and read each one directly with archive.open(info). Use a set to detect duplicates. The validation pass should raise ValueError for any disallowed member,
- 06:12
leaving the destination empty. With more complex archives, also bound total expanded bytes. The final run changes the input to a parent-path member. We should see ValueError and an empty destination. Also try an oversized member in the web task and consider a total expanded-size limit if your application accepts many files. A real importer may need a staging directory and cleanup on I/O failure; this lesson guarantees the validation policy runs before any write, not a full disk transaction. Read the extracted file back when verifying a successful archive, because a
- 06:52
filename alone does not prove the bytes were correct. After validation, create the destination directory and write each selected member. Decode the bytes with the encoding your file format promises; this lesson uses UTF-8. A UnicodeDecodeError is a useful failure because mislabeled binary data should not be accepted as text. Do not catch every exception and report success with an incomplete result. A temporary directory makes this example safe to replay. Confirm the actual file contents after writing, not only the return value of the function. For a production importer, stage files in a fresh directory and consider cleanup if
- 07:35
an I/O failure occurs mid-write. The lesson's guarantee is that policy validation precedes writes; it does not promise atomic disk transactions.
Understand the concept
The search for python zipfile extract usually begins with one line: ZipFile.extractall(destination). That line is useful for a trusted archive, but it extracts every member. This lesson answers the practical follow-up: how do you unpack only expected text files from a ZIP you did not create? We inspect each ZipInfo entry, reject unsafe names, bound uncompressed size, and then write selected bytes under a chosen directory. The browser exercise uses an in-memory ZIP and its temporary filesystem; no uploaded archive is needed.
Start by viewing archive.namelist() rather than assuming the contents. A ZIP may include directories, duplicate member names, nested paths, absolute-looking names, files you did not expect, and very large expanded content. Python's extractall performs some path normalization, but an application still needs its own acceptance policy. Here the policy is deliberately narrow: one top-level .txt filename, no slash or backslash, no dot-only name, no duplicate, and at most 4096 uncompressed bytes per file. This is a teaching policy, not a universal ZIP security limit.
Use ZipFile.infolist() so that each member's name and file_size are available before writing. Reject the archive first if any member violates the contract; do not leave a partially extracted directory when a later entry is bad. Then create the destination, read each accepted member with archive.open(info), decode UTF-8, and write it with pathlib.Path.write_text. Keeping a list of validated ZipInfo objects matters when names are duplicated: reading by name alone can be ambiguous. Close the archive with a with block even when validation raises.
The final check builds two small ZIPs in memory. One contains notes.txt and can be extracted to a temporary folder. The other contains ../outside.txt and is rejected before any write. A second policy check rejects a file whose advertised expanded size is above the limit. For real untrusted archives also consider total expanded size, compression ratio, disk capacity, symlinks, and resource limits; never assume extension alone proves that content is harmless. The earlier zipfile lesson creates archives, while this guide is specifically about inspecting and extracting their members.
- ZipFile.infolist exposes member metadata
- Validate every name before writing
- Limit expanded size
- Extract selected members with a context manager
See it step by step
Read the code, predict the output, then compare it with the result.
01. Inspect names before extraction
from io import BytesIO
from zipfile import ZipFile
buffer = BytesIO()
with ZipFile(buffer, 'w') as archive:
archive.writestr('notes.txt', 'one')
archive.writestr('data.csv', 'two')
with ZipFile(BytesIO(buffer.getvalue())) as archive:
print([item.filename for item in archive.infolist()])['notes.txt', 'data.csv']Member metadata tells you what the archive contains without writing any files.
02. Select top-level text members
names = ['notes.txt', 'dir/report.txt', 'data.csv']
print([name for name in names if name.endswith('.txt') and '/' not in name])['notes.txt']The filename policy rejects nested paths and non-text members before extraction.
03. Write a validated member
from io import BytesIO
from zipfile import ZipFile
from pathlib import Path
import tempfile
buffer = BytesIO()
with ZipFile(buffer,'w') as archive: archive.writestr('notes.txt','café')
with tempfile.TemporaryDirectory() as folder:
with ZipFile(BytesIO(buffer.getvalue())) as archive:
info = archive.infolist()[0]
Path(folder, info.filename).write_text(archive.open(info).read().decode('utf-8'), encoding='utf-8')
print(Path(folder,'notes.txt').read_text(encoding='utf-8'))caféA temporary destination makes the actual selected extraction visible and leaves no permanent file.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Inspect before you extract
Open a ZIP with a context manager and print its member names before writing files. The archive can contain more than the one document you expected. A member list is evidence about filenames, not evidence that the contents are safe. Do this first even when a download has a friendly name such as notes.zip.
ZipInfo offers more detail than namelist: filename, file_size, compress_size, and directory status. The extraction policy in this lesson accepts only one-level .txt names. That deliberate restriction also means a valid archive with nested text files will be rejected. If your application needs directories, define and test a different policy rather than silently weakening this one.
from io import BytesIO
from zipfile import ZipFile
buffer = BytesIO()
with ZipFile(buffer, 'w') as archive:
archive.writestr('notes.txt', 'Hello')
archive.writestr('image.png', b'not an image')
with ZipFile(BytesIO(buffer.getvalue())) as archive:
print([(info.filename, info.file_size) for info in archive.infolist()])[('notes.txt', 5), ('image.png', 12)]- Create an archive in memory with two different extensions.
- Inspect every ZipInfo before deciding what to write.
Validate the whole archive
Checking a filename after extracting it defeats the point of an allowlist. Validate every member before the first write. Reject empty names, names with a slash or backslash, names that are not .txt, directories, duplicates, and entries above the size limit. Compare file_size with a limit suitable for your application, not a number copied blindly from a tutorial.
A ZIP can contain repeated names. ZipFile.read('name') may select an ambiguous entry, so keep the validated ZipInfo objects and read each one directly with archive.open(info). Use a set to detect duplicates. The validation pass should raise ValueError for any disallowed member, leaving the destination empty. With more complex archives, also bound total expanded bytes.
def valid_text_member(name, size):
return (bool(name) and name not in ('.','..') and name.endswith('.txt')
and '/' not in name and '\\' not in name and ':' not in name
and 0 <= size <= 4096)
print(valid_text_member('notes.txt', 5))
print(valid_text_member('../notes.txt', 5))
print(valid_text_member('huge.txt', 4097))True
False
False- Write one filename predicate that covers every component of the policy.
- Reject a member before any file is created.
Write selected text and verify
After validation, create the destination directory and write each selected member. Decode the bytes with the encoding your file format promises; this lesson uses UTF-8. A UnicodeDecodeError is a useful failure because mislabeled binary data should not be accepted as text. Do not catch every exception and report success with an incomplete result.
A temporary directory makes this example safe to replay. Confirm the actual file contents after writing, not only the return value of the function. For a production importer, stage files in a fresh directory and consider cleanup if an I/O failure occurs mid-write. The lesson's guarantee is that policy validation precedes writes; it does not promise atomic disk transactions.
from io import BytesIO
from pathlib import Path
from zipfile import ZipFile
import tempfile
buffer = BytesIO()
with ZipFile(buffer, 'w') as archive: archive.writestr('notes.txt', 'café')
with tempfile.TemporaryDirectory() as folder:
with ZipFile(BytesIO(buffer.getvalue())) as archive:
info = archive.infolist()[0]
Path(folder, info.filename).write_text(archive.open(info).read().decode('utf-8'), encoding='utf-8')
print(Path(folder, 'notes.txt').read_text(encoding='utf-8'))café- Read the selected ZipInfo with archive.open(info).
- Write UTF-8 text beneath the chosen destination and read it back.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Inspect all ZipInfo objects first. Validate filename and file_size, then use archive.open(info) and Path(destination, name).write_text(..., encoding='utf-8').
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Extracting everything
archive.extractall(destination)Inspect infolist(), enforce a policy, then write accepted members.What happens: Unexpected members are written beside the intended text files.
The application decides which files belong in its destination.
Checking after a write
write the first file, then reject a later pathValidate every member before creating destination files.What happens: A failed operation leaves partial output.
A validation pass makes rejection predictable.