Archives
Python has two modules for handling archive files. An archive file is a file that contains an entire directory tree, as well as information about the directory tree itself. An archive file is not a directory; it is a single file which may encapsulate an entire directory tree though, which makes it useful for shipping filestore content from one place to another.
Python supports two archive file formats: zip and tar. Zip files can store compressed versions of files in a directory tree. Tar files are an archival format; they can be compressed using gzip or bzip2. Python can read both regular and compressed tar files (.tar.gz, .tgz, .tar.bz2, or .tbz).
The zipfile and tarfile modules are used for reading and writing zip and tar files, respectively. Let's take a quick look at these modules; fire up an interactive console. You'll use some of what you learned earlier to prepare a directory to archive. Let's start with tarfile. Start an interactive console session and enter the commands as shown:
>>> import os >>> import tarfile >>> import glob >>> import shutil >>> filenames = ["larry", "curly", "moe"] >>> path = "/tmp/archive_me" >>> os.mkdir(path) >>> for fn in filenames: ... f = open(os.path.join(path, fn), "w") ... f.close() ... >>> glob.glob(os.path.join(path, "*")) ['/tmp/archive_me/larry', '/tmp/archive_me/curly', '/tmp/archive_me/moe'] >>> archive_fn = "/tmp/archive_me/my_archive.tar" >>> tf = tarfile.open(archive_fn, "w") >>> tf.add(path) >>> tf.close() >>> tf = tarfile.open(archive_fn) >>> tf.list() drwxr-xr-x user/group 0 2026-06-12 08:21:08 tmp/archive_me/ -rw-r--r-- user/group 0 2026-06-12 08:21:08 tmp/archive_me/curly -rw-r--r-- user/group 0 2026-06-12 08:21:08 tmp/archive_me/larry -rw-r--r-- user/group 0 2026-06-12 08:21:08 tmp/archive_me/moe >>> tf.close() >>> archive_fn_compressed = archive_fn + ".gz" >>> tf = tarfile.open(archive_fn_compressed, "w:gz") >>> tf.add(path) >>> tf.close() >>> tf = tarfile.open(archive_fn_compressed) >>> tf.list() drwxr-xr-x user/group 0 2026-06-12 08:21:08 tmp/archive_me/ -rw-r--r-- user/group 0 2026-06-12 08:21:08 tmp/archive_me/curly -rw-r--r-- user/group 0 2026-06-12 08:21:08 tmp/archive_me/larry -rw-r--r-- user/group 0 2026-06-12 08:21:08 tmp/archive_me/moe -rw-r--r-- user/group 10240 2026-06-12 08:21:08 tmp/archive_me/my_archive.tar >>> tf.close() >>> os.path.getsize(archive_fn) 10240 >>> os.path.getsize(archive_fn_compressed) 388 >>>
| Warning | In these examples, we use "*" to add all files in a folder to an archive. If the archive is in the same folder, this can cause a serious problem when you do it again, and repeatedly, because the archive itself will be added to the archive, and you can therefore find yourself in an infinite loop creating an infinitely large archive! While it works in our limited examples, you should avoid this practice when you do real work with archives. |
Then, enter this command in the interactive Python console, as shown:
>>> shutil.rmtree(path) >>>
Just like the built-in open() function, tarfile's open() function accepts a file name and a mode. But tarfile's modes are a bit more complicated. In addition to r, w, and a for mode (read, write, and append), you must also consider access type and compression:
| Access Type | Symbol | Description |
|---|---|---|
| Block Mode | : (colon) | Opens an actual file on disk |
| Stream Mode | | (pipe) | Opens a stream, socket, or pipe |
| Compression | Symbol |
|---|---|
| GZip | gz |
| BZip2 | bz2 |
Block mode and no compression are the defaults. In our example, you used both w and w:gz to write out your tar files. The second version specifies that your tar file is compressed. At the end of your listing, where you compared the file sizes of the compressed and uncompressed archive file, the compressed version is significantly smaller.
Once you've opened your tar file for writing, you can use its add() method to add files to the archive. add() can take both filenames and directories, and by default, it adds directories recursively—if you have subdirectories in the path that you pass into add(), those subdirectories are also added to the archive. You can read tar files by using open() in read (r) mode. This is the default mode, so in the interactive shell session, we omitted the mode argument. Once you've opened a tar file, you can list its contents with the file's list() method. You can also extract its contents using its extract() or extractall() method.
Also, we used a function called rmtree() from the shutil module, to remove the directory.
| Modern Python | The code above calls tarfile.open() and then tf.close() separately.
A safer pattern uses a context manager, which closes the archive automatically even if an exception occurs:
with tarfile.open(archive_fn, "w") as tf:
tf.add(path)
Likewise for reading:
with tarfile.open(archive_fn) as tf:
tf.list()
|
| Modern Python | In Python 3.12+, extractall() and extract() accept a
filter= argument that controls which tar entries are permitted during extraction.
Passing filter='data' is the recommended safe default for untrusted archives:
with tarfile.open(archive_fn) as tf:
tf.extractall(path, filter='data')
Without a filter, Python 3.14 emits a DeprecationWarning; a future version will
require it.
|
Now we'll take a look at the zipfile module. Again, we'll use the file's name and the mode in which we open it to create an interface with zip files. But, instead of a function, the zipfile module offers a ZipFile class constructor. In an interactive shell session for zipfile, type the commands below as shown:
>>> import os, tarfile, glob, shutil, zipfile >>> filenames = ["groucho", "harpo", "chico"] >>> path = "/tmp/archive_me" >>> os.mkdir(path) >>> for fn in filenames: ... f = open(os.path.join(path, fn), "w") ... f.close() ... >>> glob.glob(os.path.join(path, "*")) ['/tmp/archive_me/harpo', '/tmp/archive_me/groucho', '/tmp/archive_me/chico'] >>> archive_fn = "/tmp/archive_me/my_archive.zip" >>> zf = zipfile.ZipFile(archive_fn, "w") >>> filenames = glob.glob(os.path.join(path, "*")) >>> for fn in filenames: ... zf.write(fn) ... >>> zf.close() >>> zf = zipfile.ZipFile(archive_fn) >>> zf.namelist() ['tmp/archive_me/harpo', 'tmp/archive_me/groucho', 'tmp/archive_me/my_archive.zip', 'tmp/archive_me/chico'] >>> #clean up. (Again, you can check your file manager first to see that the files were created.) ... >>> zf.close() >>> shutil.rmtree(path)
| Note | This time, for the sake of convenience, we added all of our imports in one line! |
One major difference between tarfile and zipfile is the method used to open the files—with zipfile, we use the class constructor instead of an open() method on an instance. As mentioned above, zip archives may contain compressed files. By default, files are stored uncompressed. To compress files, we'd pass a third argument to the class constructor—zipfile.ZIP_DEFLATED.
Unlike tarfile's add() method, ZipFile's write() method does not add files to the archive recursively. That's why we had to use glob() to get all of the files before writing them to our archive. (We'd have had to use os.path.walk or some similar functionality if there had been subdirectories to process).
You can read in a zip file by passing only the filename to the ZipFile constructor. The namelist() method lists all of the files in the archive and, just as in tarfile, the extract() method will uncompress and extract the files from the archive. Here's a quick comparison of zipfile and tarfile:
| Modern Python | As with tarfile, prefer the context-manager form for ZipFile:
with zipfile.ZipFile(archive_fn, "w", zipfile.ZIP_DEFLATED) as zf:
for fn in filenames:
zf.write(fn)
This ensures the archive is properly finalised and closed even if an exception is raised mid-loop.
|
| Function | tarfile | zipfile |
|---|---|---|
| Open for Writing | tarfile.open(fn, "w") | zipfile.ZipFile(fn, "w") |
| Open for Writing Compressed | tarfile.open(fn, "w:gz") | zipfile.ZipFile(fn, "w", zipfile.ZIP_DEFLATED) |
| Open for Reading | tarfile.open(fn) | zipfile.ZipFile(fn) |
| Add a File to the Archive | tarfile.add(path) | zipfile.ZipFile.write(path) |
| List Files in an Archive | tarfile.list() | zipfile.ZipFile.namelist() |
| Extract Files | tarfile.extract()or tarfile.extractall() | zipfile.ZipFile.extract()or zipfile.ZipFile.extractall() |
You can build on latest.py to create a function that archives the last modified files in a path. Rather than try to extend the existing test_latest module, we'll create another module to test the added functionality. For this test, create a new file named test_ziplatest.py in your Archives project. The two test modules do have some common features, but for now, we'll write a separate test suite. Enter the code for test_ziplatest.py below as shown:
import unittest
import latest
import time
import os
import shutil
import zipfile
class TestZip(unittest.TestCase):
def setUp(self):
self.path = "/tmp/zip_test"
self.zip_filename = os.path.join(self.path, "test_zip_latest.zip")
os.mkdir(self.path)
self.file_names = ["old", "newer", "newest"]
for fn in self.file_names:
f = open(os.path.join(self.path, fn), "w")
f.close()
time.sleep(1)
def test_zip_latest(self):
latest.zip_latest(self.zip_filename, 2, self.path)
zf = zipfile.ZipFile(self.zip_filename, "w")
files_in_archive = zf.namelist()
zf.close()
observed = set([os.path.basename(f) for f in files_in_archive])
expected = set(self.file_names[1:3])
self.assertEqual(observed, expected)
def tearDown(self):
os.remove(self.zip_filename)
try:
shutil.rmtree(self.path, ignore_errors=True)
except IOError:
pass
if __name__ == "__main__":
unittest.main()
Now, let's make a copy of latest.py from your FileHandling project and stub out a function. We'll call the new function zip_latest(). Modify the file as shown:
import glob
import os
def latest(num=1, path="."):
files = glob.glob(os.path.join(path, "*"))
dated_files = [(os.path.getmtime(fn), os.path.abspath(fn)) for fn in files]
dated_files.sort()
latest_files = [f for (d, f) in dated_files[-num:]]
latest_files.reverse()
return latest_files
def zip_latest(fn, num, path):
pass
A quick run will reveal a single failing test:
F
======================================================================
FAIL: test_zip_latest (__main__.TestZip)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_ziplatest.py", line 27, in test_zip_latest
self.assertEqual(observed, expected)
AssertionError: Items in the second set but not the first:
'newest'
'newer'
----------------------------------------------------------------------
Ran 1 test in 3.032s
FAILED (failures=1)
Now that the test program has created the zip file, we can change it from write mode to read mode. Edit test_ziplatest.py as shown:
import unittest
import latest
import time
import os
import shutil
import zipfile
class TestZip(unittest.TestCase):
def setUp(self):
self.path = "/tmp/zip_test"
self.zip_filename = os.path.join(self.path, "test_zip_latest.zip")
os.mkdir(self.path)
self.file_names = ["old", "newer", "newest"]
for fn in self.file_names:
f = open(os.path.join(self.path, fn), "w")
f.close()
time.sleep(1)
def test_zip_latest(self):
latest.zip_latest(self.zip_filename, 2, self.path)
zf = zipfile.ZipFile(self.zip_filename, "r")
files_in_archive = zf.namelist()
zf.close()
observed = set([os.path.basename(f) for f in files_in_archive])
expected = set(self.file_names[1:3])
self.assertEqual(observed, expected)
def tearDown(self):
os.remove(self.zip_filename)
try:
shutil.rmtree(self.path, ignore_errors=True)
except IOError:
pass
if __name__ == "__main__":
unittest.main()
Most of the functionality you need is already within your module. Combine what you've learned about archive files with your latest(), and add a few lines to latest.py, as shown:
import glob
import os
import zipfile
def latest(num=1, path="."):
files = glob.glob(os.path.join(path, "*"))
dated_files = [(os.path.getmtime(fn), os.path.abspath(fn)) for fn in files]
dated_files.sort()
latest_files = [f for (d, f) in dated_files[-num:]]
latest_files.reverse()
return latest_files
def zip_latest(fn, num, path):
files_to_archive = latest(num, path)
zf = zipfile.ZipFile(fn, "w", zipfile.ZIP_DEFLATED)
for fn_to_archive in files_to_archive:
zf.write(fn_to_archive)
zf.close()
If the tests pass, your changes to the latest module have worked. Congratulations!
Now you've got a good foundation for two archive file formats: zip and tar. We used Python's zipfile and tarfile modules to read and write each format. Finally, we integrated this knowledge to write a quick function that archived the latest n files in a path.
Great work so far! Keep it up!
