Skip to content
Python Basics: Copying and Moving Files with shutil

Python Basics: Copying and Moving Files with shutil

October 13, 2026

Preparing sample folders is a useful way to learn file automation. Python’s standard-library shutil module supplies copying, moving, and directory-removal operations. Here, every operation uses disposable data.

Before you start

These three independent examples were tested on Linux with Bash, Python 3.12.14 and Python 3.13.5. No extra packages or environment variables are required. The commands use python3; substitute python3.13 to run the other tested interpreter.

Create a fresh working directory in one Bash session:

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

Save each UTF-8 source file below here. Each script creates its own temporary data directory inside this working directory and removes that data when its with block finishes. Do not replace these paths with important folders. Avoid naming a script shutil.py, which would shadow the module.

Copy a file and inspect an overwrite

Save as demo_shutil_1.py:

from pathlib import Path
from tempfile import TemporaryDirectory
import shutil

with TemporaryDirectory(dir=".") as directory:
    root = Path(directory)
    source = root / "notes.txt"
    target = root / "notes-copy.txt"
    source.write_text("new notes\n", encoding="utf-8")
    target.write_text("old notes\n", encoding="utf-8")

    print("Before:", target.read_text(encoding="utf-8").strip())
    shutil.copyfile(source, target)
    print("After:", target.read_text(encoding="utf-8").strip())
    print("Source remains:", source.exists())

    try:
        shutil.copyfile(source, source)
    except shutil.SameFileError:
        print("Same-file copy: rejected")

Run it:

python3 demo_shutil_1.py

Output:

Before: old notes
After: new notes
Source remains: True
Same-file copy: rejected

copyfile() replaces existing destination content and needs a complete target filename. It copies contents, not metadata. copy2() attempts metadata preservation, but cannot preserve everything. The same-file error is intentional and caught. See the copying documentation.

Copy a folder while skipping scratch files

Save as demo_shutil_2.py:

from pathlib import Path
from tempfile import TemporaryDirectory
import shutil

with TemporaryDirectory(dir=".") as directory:
    root = Path(directory)
    source = root / "project"
    (source / "docs").mkdir(parents=True)
    (source / "README.txt").write_text("Sample project\n", encoding="utf-8")
    (source / "notes.tmp").write_text("Scratch data\n", encoding="utf-8")
    (source / "docs" / "guide.txt").write_text("Start here\n", encoding="utf-8")
    target = root / "project-copy"

    shutil.copytree(source, target, ignore=shutil.ignore_patterns("*.tmp"))
    copied = sorted(
        path.relative_to(target).as_posix()
        for path in target.rglob("*")
        if path.is_file()
    )
    print("Copied:", copied)
    print("Scratch copied:", (target / "notes.tmp").exists())

    try:
        shutil.copytree(source, target)
    except FileExistsError:
        print("Existing destination: rejected")

Run it:

python3 demo_shutil_2.py

Output:

Copied: ['README.txt', 'docs/guide.txt']
Scratch copied: False
Existing destination: rejected

copytree() copies recursively; its default requires a new destination. dirs_exist_ok=True permits existing directories and overwrites matching files, so it is deliberately omitted. Default copying follows symbolic links; this sample creates none. See the directory-copy documentation.

The sorted relative names make this small folder easy to inspect without displaying a random temporary path.

Move finished work, then remove disposable data

Save as demo_shutil_3.py:

from pathlib import Path
from tempfile import TemporaryDirectory
import shutil

with TemporaryDirectory(dir=".") as directory:
    root = Path(directory)
    source = root / "report.txt"
    source.write_text("Finished report\n", encoding="utf-8")
    archive = root / "archive"
    archive.mkdir()

    shutil.move(source, archive)
    print("Original exists:", source.exists())
    print("Archived:", (archive / "report.txt").read_text(encoding="utf-8").strip())

    shutil.rmtree(archive)
    print("Archive exists:", archive.exists())

Run it:

python3 demo_shutil_3.py

Output:

Original exists: False
Archived: Finished report
Archive exists: False

Moving into an existing directory retains the source filename. Existing file destinations may be overwritten, depending on platform behavior. Across filesystems, a move can involve copying followed by removal. rmtree() deletes a whole directory tree without a recycle bin. See the move and removal documentation.

The archive above contains only the report we just created. For real work, review both paths and keep backups before changing or removing files.

Clean up

The scripts remove their temporary data automatically. In the same Bash session, delete only their source files, then remove the empty working directory:

rm -- demo_shutil_1.py demo_shutil_2.py demo_shutil_3.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

If rmdir reports remaining files, inspect them rather than adding recursive deletion.

References

Acknowledgements

This post was created with assistance from AI.

Last updated on