Python Basics: Copying and Moving Files with shutil
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 1Save 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.pyOutput:
Before: old notes
After: new notes
Source remains: True
Same-file copy: rejectedcopyfile() 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.pyOutput:
Copied: ['README.txt', 'docs/guide.txt']
Scratch copied: False
Existing destination: rejectedcopytree() 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.pyOutput:
Original exists: False
Archived: Finished report
Archive exists: FalseMoving 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_dirIf rmdir reports remaining files, inspect them rather than adding recursive deletion.
References
Acknowledgements
This post was created with assistance from AI.