How to Copy Files to Another Directory in Python

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:

VS Code output of a Python script copying four JPG photos into a delivery folder and skipping all four on the second run
First run: 4 copied, 0 skipped. Second run: 0 copied, 4 skipped, so nothing gets overwritten.

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:

ItemTypeNotes
IMG_2041.jpg to IMG_2044.jpgJPEG photosThe files the client gets; shot on 2026-10-24
clip_0007.movVideoNot part of the delivery
shot_list.txtTextCeremony and portrait times
Thumbs.dbWindows cacheJunk we never want to copy
raw/IMG_2041.CR3, raw/IMG_2042.CR3SubfolderRaw 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.

  1. Create a file named copy_one.py next to the camera folder.
  2. Paste the code below and save it.
  3. Run it with python copy_one.py in 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']
VS Code output of Python shutil.copy copying IMG_2041.jpg into the delivery folder and returning the new path
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
VS Code output of a Python loop using Path.glob to copy four JPG files from camera to delivery
Only the four .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
VS Code output comparing the modified date after Python shutil.copy and shutil.copy2 on the same photo
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
VS Code output showing Python shutil.copy silently replacing an edited file, then a loop skipping files that already exist
The edited file goes from 81,000 to 48,213 bytes with no warning; the safe loop skips it.

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</