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

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: True

2 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: True

3 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

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

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.