High-Level File Operations (shutil)
Reviewed & published by Brayan K
The shutil module provides high-level file operations — copying, moving, archiving, and deleting whole files and directory trees — in a single, convenient function call each.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Where os gives you low-level building blocks, shutil gives you the "do what I mean" verbs: copy a folder, zip a directory, move a file, free-space check. We'll demo everything safely inside a temporary directory.
What You'll Learn in This Lesson
- Copying files with copy and copy2
- Copying whole trees with copytree
- Moving and renaming with move
- Deleting directories with rmtree
- Zipping with make_archive / unpack_archive
- Checking space with disk_usage and finding tools with which
1 Copying Files: copy and copy2
shutil.copy(src, dst) duplicates a file's contents and permissions; copy2 additionally preserves timestamps and metadata. We work inside a TemporaryDirectory so nothing is left behind:
import shutil, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
base = Path(d)
src = base / "original.txt"
src.write_text("important data")
dst = base / "backup.txt"
shutil.copy2(src, dst) # also preserves timestamps
print("Backup content:", dst.read_text())
print("Both exist:", src.exists() and dst.exists())import shutil, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
base = Path(d)
src = base / "original.txt"
src.write_text("important data")
dst = base / "backup.txt"
shutil.copy2(src, dst)
print("Backup content:", dst.read_text())
print("Both exist:", src.exists() and dst.exists())
# Expected output:
# Backup content: important data
# Both exist: True2 Whole Trees: copytree, move, rmtree
These three handle entire directory trees. copytree recursively copies a folder, move relocates or renames it, and rmtree deletes it and everything inside:
import shutil, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
base = Path(d)
project = base / "project"
project.mkdir()
(project / "main.py").write_text("print('hi')")
(project / "data.txt").write_text("rows")
# Copy an entire tree
copy = base / "project_copy"
shutil.copytree(project, copy)
print("Copied files:", sorted(p.name for p in copy.iterdir()))
# Move (rename) it
moved = base / "renamed"
shutil.move(str(copy), str(moved))
print("Moved exists:", moved.exists(), "old gone:", not copy.exists())
# Recursively delete
shutil.rmtree(moved)
print("Deleted:", not moved.exists())import shutil, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
base = Path(d)
project = base / "project"
project.mkdir()
(project / "main.py").write_text("print('hi')")
(project / "data.txt").write_text("rows")
copy = base / "project_copy"
shutil.copytree(project, copy)
print("Copied files:", sorted(p.name for p in copy.iterdir()))
moved = base / "renamed"
shutil.move(str(copy), str(moved))
print("Moved exists:", moved.exists(), "old gone:", not copy.exists())
shutil.rmtree(moved)
print("Deleted:", not moved.exists())
# Expected output:
# Copied files: ['data.txt', 'main.py']
# Moved exists: True old gone: True
# Deleted: True3 Archives, disk_usage, and which
make_archive zips a folder and returns the new archive's path; unpack_archive extracts it again. Two handy utilities round things out: disk_usage reports free space, and which locates an executable on PATH:
import shutil, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
base = Path(d)
folder = base / "stuff"
folder.mkdir()
(folder / "a.txt").write_text("aaa")
(folder / "b.txt").write_text("bbb")
# Zip the folder
archive_path = shutil.make_archive(str(base / "stuff_backup"), "zip", folder)
print("Archive created:", Path(archive_path).name)
# Unpack it elsewhere
out = base / "restored"
shutil.unpack_archive(archive_path, str(out))
print("Restored files:", sorted(p.name for p in out.iterdir()))import shutil
usage = shutil.disk_usage("/")
print("Has fields:", usage.total > 0 and usage.free >= 0)
python_path = shutil.which("python3")
print("Found python3:", python_path is not None)import shutil, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
base = Path(d)
folder = base / "stuff"
folder.mkdir()
(folder / "a.txt").write_text("aaa")
(folder / "b.txt").write_text("bbb")
archive_path = shutil.make_archive(str(base / "stuff_backup"), "zip", folder)
print("Archive created:", Path(archive_path).name)
out = base / "restored"
shutil.unpack_archive(archive_path, str(out))
print("Restored files:", sorted(p.name for p in out.iterdir()))
# Expected output:
# Archive created: stuff_backup.zip
# Restored files: ['a.txt', 'b.txt']🎯 Your Turn: Fill in the Blanks
Replace each ___ so the code makes a backup copy of a config file and confirms it matches.
import shutil, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
base = Path(d)
src = base / "config.ini"
src.write_text("[settings]\ndebug=true\n")
dst = base / "config.bak"
shutil.___(src, dst) # hint: copy a single file
print("Backup made:", dst.exists())
print("Same content:", src.read_text() == dst.read_text())
# Expected output:
# Backup made: True
# Same content: True⚠️ Common Errors
❌ Using copy on a directory
shutil.copy("my_folder", "dest") # IsADirectoryError✅ Use copytree for folders
shutil.copytree("my_folder", "dest")❌ copytree onto an existing folder (older Python)
shutil.copytree(src, existing_dir) # FileExistsError✅ Allow merging into it
shutil.copytree(src, existing_dir, dirs_exist_ok=True)❌ Adding the extension to make_archive yourself
shutil.make_archive("backup.zip", "zip", folder) # -> backup.zip.zip✅ Leave the extension off
shutil.make_archive("backup", "zip", folder) # -> backup.zip🎯 Mini Challenge: Mini Deploy Pipeline
Build a tiny "site", archive it, copy it to a release folder, then clean up the source — all inside a temp directory.
import shutil, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
base = Path(d)
site = base / "site"
site.mkdir()
for name in ["index.html", "about.html", "style.css"]:
(site / name).write_text(f"content of {name}")
# 1. Archive the whole site
zip_path = shutil.make_archive(str(base / "site_v1"), "zip", site)
# 2. Deploy = copy the tree to a release folder
release = base / "release"
shutil.copytree(site, release)
# 3. Report
print("Archive:", Path(zip_path).name)
print("Released files:", len(list(release.iterdir())))
# 4. Clean up the source build
shutil.rmtree(site)
print("Source removed:", not site.exists())
# Expected output:
# Archive: site_v1.zip
# Released files: 3
# Source removed: True📚 Keep Learning
- • shutil — High-level file operations
- • shutil — Archiving operations
- • os — Low-level filesystem building blocks
Frequently Asked Questions
Lesson complete — you wrangle files like a pro!
You can copy files with copy/copy2, copy and delete whole trees with copytree/rmtree, relocate with move, zip and unzip with make_archive/unpack_archive, and check space and tools with disk_usage and which.
Practice quiz
What does shutil.copy2 preserve that shutil.copy does not?
- The file contents
- The file permissions
- Timestamps and metadata
- Nothing different
Answer: Timestamps and metadata. Both copy contents and permission bits; copy2 additionally preserves timestamps and metadata.
What happens if you pass a directory to shutil.copy?
- It raises an error (IsADirectoryError)
- It copies the whole tree
- It copies only the first file
- It creates an empty folder
Answer: It raises an error (IsADirectoryError). copy and copy2 work on single files only; use copytree for directories.
Which function recursively copies a directory and everything inside it?
- shutil.copy
- shutil.copyfile
- shutil.duplicate
- shutil.copytree
Answer: shutil.copytree. shutil.copytree(src, dst) recursively copies a folder and all its contents.
What does shutil.rmtree(path) do?
- Moves a directory
- Permanently deletes a directory and all its contents
- Empties a file
- Renames a directory
Answer: Permanently deletes a directory and all its contents. rmtree recursively deletes a directory tree permanently — no recycle bin, no undo.
Which function moves or renames a file or directory?
- shutil.move
- shutil.rename
- shutil.shift
- shutil.relocate
Answer: shutil.move. shutil.move(src, dst) relocates or renames files and whole directory trees.
What does shutil.make_archive("backup", "zip", folder) return?
- None
- The folder contents
- The path to the created archive (e.g. backup.zip)
- True or False
Answer: The path to the created archive (e.g. backup.zip). It builds the archive and returns its full path, adding the .zip extension for you.
How should you name the first argument to make_archive?
- With the extension, e.g. "backup.zip"
- Without the extension, e.g. "backup"
- As a folder path only
- It must be empty
Answer: Without the extension, e.g. "backup". Leave the extension off — shutil adds it. "backup.zip" would produce backup.zip.zip.
Which function extracts an archive back into a directory?
- shutil.extract
- shutil.unzip
- shutil.open_archive
- shutil.unpack_archive
Answer: shutil.unpack_archive. shutil.unpack_archive(filename, extract_dir) reverses make_archive.
What does shutil.disk_usage(path) return?
- A list of files
- A named tuple of total, used, and free bytes
- The folder size only
- A boolean
Answer: A named tuple of total, used, and free bytes. It reports total, used, and free bytes for the filesystem containing that path.
What does shutil.which("python3") do?
- Runs python3
- Installs python3
- Returns the full path of the executable on PATH, or None
- Lists Python versions
Answer: Returns the full path of the executable on PATH, or None. which searches PATH for an executable and returns its location, like the Unix 'which' command.
Continue this course
- Previous: Finding Files with glob & fnmatch
- Next: Network Programming with sockets — Build TCP clients and servers with Python's socket module
- Quick reference: Python cheat sheet
Frequently asked questions
What's the difference between shutil.copy and shutil.copy2?
Both copy a file's contents and its permission bits. The difference is metadata: copy2 also preserves timestamps (the last-modified and last-access times) and other file metadata, whereas copy does not. When you want an as-faithful-as-possible duplicate — for backups, for example — reach for copy2.
How do I copy or delete an entire folder tree?
Use shutil.copytree(src, dst) to recursively copy a directory and everything inside it, and shutil.rmtree(path) to recursively delete a directory and all its contents. Be careful with rmtree — it deletes permanently with no recycle bin, so always double-check the path you pass it.
How do I make a zip archive of a folder?
shutil.make_archive(base_name, format, root_dir) builds an archive ('zip', 'tar', 'gztar', and others) from a directory and returns the path to the new file. shutil.unpack_archive(filename, extract_dir) reverses it, extracting an archive into a directory. Together they're a one-line backup-and-restore.
What do disk_usage and which do?
shutil.disk_usage(path) returns a named tuple with the total, used, and free bytes for the filesystem containing that path — handy before writing big files. shutil.which(name) searches the system PATH for an executable and returns its full location, mirroring the Unix 'which' command. Both are quick utilities you'll reach for often.