A wedding photographer in Austin asked me to copy files to another directory in Python after every shoot: the JPEGs from her camera folder go into the client’s delivery folder, and an edited photo never gets overwritten.
The answer is shutil.copy2(src, dest_folder) inside a loop, with an exists() check before each copy. It copies the file into the folder, keeps the name and keeps the shoot date.
In this tutorial, I’ll build that script step by step, and you’ll learn how to copy files to another directory in Python: one file, a filtered set, a whole folder tree, and the new Python 3.14 Path.copy_into().
Here’s what we’re building. The first run copies four photos; the second run copies nothing, because they’re already there:

Every result below comes from real runs on Python 3.12.5 on Windows 11 (plus Python 3.14.8 for the pathlib section), shown in VS Code.
Before You Start: the sample files to copy
You need Python 3.8 or newer for dirs_exist_ok and 3.14 for the pathlib methods. The shutil module ships with Python, so there’s nothing to install. If you’re not sure the folders exist yet, check if a directory exists in Python first.
Our running sample is a folder called camera, right next to the script. Here’s what’s inside it:
| Item | Type | Notes |
|---|---|---|
IMG_2041.jpg to IMG_2044.jpg | JPEG photos | The files the client gets; shot on 2026-10-24 |
clip_0007.mov | Video | Not part of the delivery |
shot_list.txt | Text | Ceremony and portrait times |
Thumbs.db | Windows cache | Junk we never want to copy |
raw/IMG_2041.CR3, raw/IMG_2042.CR3 | Subfolder | Raw files for the backup only |
This short script prints the folder so you can confirm your copy matches mine:
from pathlib import Path
for item in sorted(Path("camera").rglob("*")):
size = "folder" if item.is_dir() else f"{item.stat().st_size:,} bytes"
print(f"{item.as_posix():<24} {size}")
Output:
camera/clip_0007.mov 92,160 bytes
camera/IMG_2041.jpg 48,213 bytes
camera/IMG_2042.jpg 51,907 bytes
camera/IMG_2043.jpg 46,380 bytes
camera/IMG_2044.jpg 53,122 bytes
camera/raw folder
camera/raw/IMG_2041.CR3 81,920 bytes
camera/raw/IMG_2042.CR3 84,992 bytes
camera/shot_list.txt 76 bytes
camera/Thumbs.db 3,072 bytes
That’s 9 files, two of them inside the raw subfolder. To list a folder like this in other ways, see how to list files in a directory with Python.
Step 1: Copy one file to another directory with shutil.copy()
The goal of this step is the smallest possible copy: one photo into a folder called delivery.
- Create a file named
copy_one.pynext to thecamerafolder. - Paste the code below and save it.
- Run it with
python copy_one.pyin the VS Code terminal.
import shutil
from pathlib import Path
dest = Path("delivery")
dest.mkdir(exist_ok=True)
result = shutil.copy("camera/IMG_2041.jpg", dest)
print("copied to:", result)
print("delivery now has:", [p.name for p in dest.iterdir()])
Output:
copied to: delivery\IMG_2041.jpg
delivery now has: ['IMG_2041.jpg']

shutil.copy() returns the path of the new file, here delivery\IMG_2041.jpg.When the second argument is a folder, shutil.copy() keeps the original file name. It returns the new path, which is handy for logging.
mkdir(exist_ok=True) matters here (without it, a first run on a fresh machine fails because delivery doesn’t exist).
Checkpoint: delivery now holds exactly one file, IMG_2041.jpg.
Need the copy under a different name? Pass a full file path instead of a folder; my guide on copying a file and renaming it in Python covers that case.
Step 2: Copy all JPG files to another directory with glob
Same camera folder, but now we want every photo and nothing else. Path.glob("*.jpg") picks the matching files, and the loop copies each one:
import shutil
from pathlib import Path
src, dest = Path("camera"), Path("delivery")
dest.mkdir(exist_ok=True)
for photo in sorted(src.glob("*.jpg")):
shutil.copy(photo, dest)
print("copied", photo.name)
print(len(list(dest.iterdir())), "files in", dest)
Output:
copied IMG_2041.jpg
copied IMG_2042.jpg
copied IMG_2043.jpg
copied IMG_2044.jpg
4 files in delivery

.jpg files are copied; the video, text file, cache and raw folder stay behind.sorted() isn’t required, but it makes the order predictable, so the log reads IMG_2041 to IMG_2044.
Checkpoint: delivery holds 4 files, and clip_0007.mov isn’t one of them.
If you prefer the older os style, os.listdir() plus os.path.isfile() copies every file in the top level while skipping subfolders:
import os
import shutil
src, dest = "camera", "delivery"
os.makedirs(dest, exist_ok=True)
for name in os.listdir(src):
path = os.path.join(src, name)
if os.path.isfile(path): # skips the raw/ folder
shutil.copy(path, dest)
print(sorted(os.listdir(dest)))
Output:
['IMG_2041.jpg', 'IMG_2042.jpg', 'IMG_2043.jpg', 'IMG_2044.jpg', 'Thumbs.db', 'clip_0007.mov', 'shot_list.txt']
That copies all 7 top-level files, including Thumbs.db, which is why a pattern is usually better. Building paths with os.path.join in Python keeps this version working on Windows, macOS and Linux.
Step 3: Keep the original date with shutil.copy2()
Photographers sort by date, so the copies should keep the shoot date. Here’s what each function does to the modified time of IMG_2042.jpg:
import shutil
import time
from pathlib import Path
src = Path("camera/IMG_2042.jpg")
dest = Path("delivery")
dest.mkdir(exist_ok=True)
a = shutil.copy(src, dest / "with_copy.jpg")
b = shutil.copy2(src, dest / "with_copy2.jpg")
def day(p):
return time.strftime("%Y-%m-%d", time.localtime(Path(p).stat().st_mtime))
for p in (src, a, b):
print(f"{Path(p).name:<15} modified {day(p)}")
Output:
IMG_2042.jpg modified 2026-10-24
with_copy.jpg modified 2026-10-31
with_copy2.jpg modified 2026-10-24

copy() stamps the copy with today’s date; copy2() keeps the source’s 2026-10-24 date.shutil.copy() copies the content and the permission bits. shutil.copy2() also copies the timestamps and other metadata the system allows.
Checkpoint: with_copy2.jpg shows 2026-10-24, the same day as the source.
Note: neither function copies the owner or the Windows security settings (ACLs). That’s documented, not a bug.
Step 4: Stop shutil.copy() from overwriting existing files
This is the step that protects the client’s edits. Say the client already retouched IMG_2041.jpg in the delivery folder (we fake that with write_bytes()). Watch what a plain copy does:
import shutil
from pathlib import Path
dest = Path("delivery")
dest.mkdir(exist_ok=True)
edited = dest / "IMG_2041.jpg"
edited.write_bytes(b"retouched" * 9000) # the client's edited copy
print("before:", f"{edited.stat().st_size:,}", "bytes")
shutil.copy("camera/IMG_2041.jpg", dest) # no warning at all
print("after: ", f"{edited.stat().st_size:,}", "bytes")
print("--- safe version ---")
for photo in sorted(Path("camera").glob("IMG_204[12].jpg")):
target = dest / photo.name
if target.exists():
print("skip", photo.name, "(already delivered)")
continue
shutil.copy2(photo, target)
print("copy", photo.name)
Output:
before: 81,000 bytes
after: 48,213 bytes
--- safe version ---
skip IMG_2041.jpg (already delivered)
copy IMG_2042.jpg

shutil.copy() replaced the 81,000-byte edited file with the 48,213-byte original and said nothing. The safe loop checks target.exists() first, skips IMG_2041.jpg and copies only IMG_2042.jpg.
Checkpoint: the second half of the output says skip IMG_2041.jpg and copy IMG_2042.jpg.
The same exists-then-act idea shows up in checking if a file exists in Python