Code of the Day
BeginnerAutomation Thinking

Automating file operations

Use pathlib to find files by pattern and shutil to copy, move, or rename them in bulk.

WorkflowBeginner10 min read
Recommended first
By the end of this lesson you will be able to:
  • Use pathlib.Path.glob() to iterate over files matching a pattern
  • Construct output paths relative to an input path
  • Copy a file with shutil.copy() and move or rename one with shutil.move()

Now that you understand paths, you can write code that finds files automatically and does something with each one. The two modules you need are already in the standard library: for finding and building paths, and shutil for copying, moving, and deleting files.

Finding files with glob

Path.glob() takes a pattern and yields every matching path in that directory:

from pathlib import Path

data_dir = Path("data")

# All .csv files directly inside data/
for csv_file in data_dir.glob("*.csv"):
    print(csv_file)

# All .csv files anywhere inside data/ and its subdirectories
for csv_file in data_dir.glob("**/*.csv"):
    print(csv_file)

The ** pattern means "any number of directory levels." This is the same glob syntax used in shells and .gitignore files.

Building output paths

A common pattern: for each input file, construct a corresponding output path that keeps the same filename but lands in a different directory:

from pathlib import Path

input_dir = Path("raw")
output_dir = Path("processed")
output_dir.mkdir(exist_ok=True)   # create if it doesn't exist

for f in input_dir.glob("*.csv"):
    out = output_dir / f.name     # same filename, different directory
    print(f"Would copy {f} -> {out}")

f.name is just the filename without the directory part. output_dir / f.name joins them cleanly without any string concatenation.

Copying and moving with shutil

import shutil
from pathlib import Path

src = Path("data/report.csv")
dst = Path("archive/report_backup.csv")

shutil.copy(src, dst)      # copy src to dst (creates dst)
shutil.move(src, dst)      # move (rename) src to dst

shutil.copy copies the file contents. shutil.move moves the file — equivalent to a rename if source and destination are on the same filesystem.

shutil.move on a destination that already exists will silently overwrite it on most platforms. Check dst.exists() first if you want to avoid clobbering files.

Try it

The runner below demonstrates the path-building and glob logic using in-memory string paths — no real filesystem needed:

Python — editable, runs in your browser

PurePosixPath works identically to Path for building and inspecting paths — it just doesn't touch the real filesystem, which is why it works in the browser runner.

A bulk rename pattern

Here is the complete shape of a bulk rename script — find every draft file, rename it to final:

from pathlib import Path

target_dir = Path("documents")
for f in target_dir.glob("draft_*.txt"):
    new_name = f.with_name(f.name.replace("draft_", "final_"))
    f.rename(new_name)
    print(f"renamed {f.name} -> {new_name.name}")

f.with_name(...) creates a new path in the same directory with a different filename — cleaner than rebuilding the path from scratch.

Where to go next

Next: what makes a good script — before writing a full script, understand the four properties that separate scripts you trust from scripts that cause surprises.

Finished reading? Mark it complete to track your progress.

On this page