File and Folder Automation with pathlib

Find, sort, and plan file changes safely

Intermediate 11 min

In this lesson

pathlib is Python’s modern, cross-platform way to point at files and folders and read their parts — without wrestling with slashes — and most automation eventually touches files. Here you’ll parse filenames, sort them by type, and build a dry-run plan you can trust before moving anything.

Explain it like I’m 5

A Path is a smart label for a file or folder. Instead of chopping up strings yourself, you ask the label questions: what’s your name? your extension? what folder are you in?

Paths as objects, not strings

Path turns a path into an object you can question. The handy parts are .name (the full filename), .stem (the name without its extension), and .suffix (the extension, including the dot). Reading just the parts needs no real file on disk, so it works anywhere.

Example
from pathlib import Path

p = Path("photos/vacation.JPG")
print(p.name)
print(p.stem)
print(p.suffix)
print(p.suffix.lower())
Output
vacation.JPG
vacation
.JPG
.jpg
One Path, four useful pieces.

Reading file content

Once you have a Path, opening and reading it is straightforward. path.read_text() loads the whole file as a string in one call — convenient for small config files or templates. For anything line-by-line, path.open() gives you a file object you can loop, and Python reads one line at a time so the whole file is never in memory at once.

Example
from pathlib import Path

# Simulate writing a file so this runs in the sandbox:
p = Path("notes.txt")
p.write_text("line one\nline two\nline three\n")

# read_text() loads the whole file into a string:
content = p.read_text()
print(repr(content))
Output
'line one\nline two\nline three\n'
read_text() loads the whole file at once — fine for small files.
Example
from pathlib import Path

p = Path("notes.txt")
p.write_text("line one\nline two\nline three\n")

# path.open() lets you loop line by line:
with p.open("r", encoding="utf-8") as f:
    for line in f:
        print(line.strip())
Output
line one
line two
line three
Looping an open file reads one line at a time — never the whole file.

Open notes.txt with path.open() and print each line stripped of whitespace. The file is written for you here — on a real machine you'd just point Path at an existing file and the rest of the code would be identical.

from pathlib import Path

# Written here to stand in for a file already on disk.
p = Path("notes.txt")
p.write_text("alpha\nbeta\ngamma\n")

# TODO: open p and print each line stripped of whitespace

Group files by extension

To file every document into a folder named after its type, derive the folder from each file’s suffix. Lower-casing and stripping the dot turns .JPG into a clean folder name like jpg.

Example
from pathlib import Path

files = ["a.txt", "b.jpg", "notes.txt", "pic.JPG"]
for name in files:
    folder = Path(name).suffix.lower().lstrip(".") or "other"
    print(f"{name} -> {folder}/{name}")
Output
a.txt -> txt/a.txt
b.jpg -> jpg/b.jpg
notes.txt -> txt/notes.txt
pic.JPG -> jpg/pic.JPG
Each file is mapped to a type folder — but nothing moves yet.

Always dry-run before you move anything

Finding files on a real machine uses globbing — matching by pattern, like every .txt in a folder. Whether you find files by glob or get them as a list, the rule is the same: print the plan first, move second.

Example
from pathlib import Path

# On your computer this lists every .txt file in the current folder:
for path in Path(".").glob("*.txt"):
    print(path.name)
Globbing finds files by pattern — run this in a real folder to see results.

Finish organize_plan so it returns a list of "name -> folder/name" strings, filing each file into a folder named after its lowercase extension. Don't move anything.

from pathlib import Path

def organize_plan(files):
    plan = []
    for name in files:
        folder = ""  # TODO: lowercase extension without the dot, e.g. "txt"
        plan.append(f"{name} -> {folder}/{name}")
    return plan

print(organize_plan(["report.PDF", "photo.jpg"]))

Common mistake: Running a destructive change on the wrong folder

Why it happens:

A single wrong path, or running from the wrong directory, points the script at files you didn’t mean to touch.

How to fix it:

Print the resolved folder and the plan first, and start with a dry run. Confirm the target is what you expect before any move or delete.

Common mistake: Assuming every file has an extension

Why it happens:

Files like README or Makefile have an empty .suffix, which breaks code that expects a real type.

How to fix it:

Handle the empty case explicitly — for example suffix.lstrip(".") or "other" — so extension-less files still go somewhere sensible.

Common mistake: Building paths by gluing strings together

Why it happens:

Writing folder + "/" + name seems simple but bakes in one operating system’s slash and misses edge cases.

How to fix it:

Join paths with the / operator on Path objects (Path(folder) / name); it’s readable and works on every platform.

What does Path("a/b.txt").suffix return?

Why run a dry-run before renaming or moving files?

What does globbing do?

Mini exercise (medium)

Write count_by_type(files) that takes a list of filenames and returns a dict mapping each lowercase extension (without the dot) to how many files have it. Files with no extension should count under "none".

Do it here. Finish the code below and press Run to try your answer right in the browser — no setup needed.

from pathlib import Path

def count_by_type(files):
    counts = {}
    for name in files:
        ext = ""  # TODO: lowercase extension without the dot, or "none" if there isn't one
        # TODO: add 1 to counts[ext]
    return counts

print(count_by_type(["a.txt", "b.TXT", "c.jpg", "README"]))

What to learn next

You met pathlib: paths as objects with .name, .stem, and .suffix, grouping files by their lowercase extension, globbing to find files by pattern, and always building a dry-run plan before moving anything. You wrote count_by_type() to tally files by extension.

Those files can get big. Generators and the yield Keyword shows how to stream through data one item at a time with yield — so a script can scan a huge file without ever loading it all into memory. When you’re ready, continue to the next lesson.