Learning Goals
3 minBy the end of this lesson you can:
- Build paths with the
/operator instead of string concatenation. - Read the parts of a path:
.name,.stem,.suffix,.parent. - Check, create, and delete files and folders with
Pathmethods. - Read and write whole files in a single line with
read_text/write_text.
Warm-Up · The String-Glue Trap
5 minHere's how people used to build paths — and why it hurts:
path = folder + "/" + name + ".txt" # breaks on Windows (uses \) path = folder + name # missing separator → bug path = "data" + "/" + "2026" + "/" + f # ugly and error-prone
A path isn't really a string — it's a structured thing with a parent, a name, an extension. pathlib.Path treats it that way. You build paths with /, ask them questions in plain English, and the library handles OS differences (/ vs \\) for you.
New Concept · Path Objects
14 minBuilding paths with /
from pathlib import Path base = Path("data") file = base / "2026" / "report.csv" # the / operator joins parts print(file) # data/2026/report.csv (or data\... on Windows)
The / operator is overloaded for paths — it inserts the correct separator for your OS. No more guessing slashes.
Inspecting a path
p = Path("data/2026/report.csv") print(p.name) # report.csv ← file name with extension print(p.stem) # report ← name without extension print(p.suffix) # .csv ← the extension print(p.parent) # data/2026 ← containing folder print(p.parts) # ('data', '2026', 'report.csv')
These read like English and replace a tangle of os.path.basename, splitext, and string slicing.
Useful starting points
Path.cwd() # current working directory Path.home() # the user's home folder Path(__file__) # the script's own location Path("~/notes").expanduser() # expand ~ to the home folder
Asking questions
p = Path("report.csv") p.exists() # True if it exists at all p.is_file() # True if it's a file p.is_dir() # True if it's a folder p.absolute() # the full path from the root
Creating and removing
out = Path("output/reports") out.mkdir(parents=True, exist_ok=True) # make folder + parents, don't error if it exists (out / "log.txt").touch() # create an empty file (out / "log.txt").unlink() # delete a file out.rmdir() # delete an EMPTY folder
parents=True creates intermediate folders; exist_ok=True means "fine if it's already there." Together they make mkdir safe to call repeatedly.
Read and write in one line
p = Path("note.txt") p.write_text("Hello, automation!", encoding="utf-8") # write whole file content = p.read_text(encoding="utf-8") # read whole file p.write_bytes(b"\x00\x01") # binary write data = p.read_bytes() # binary read
For small files this is far cleaner than open()/read()/close(). Always pass encoding="utf-8" for text so your code behaves the same everywhere.
os.pathIf a library hands you an old-style string path, wrap it: Path(some_string). If a library demands a string, pass str(my_path). Modern Python accepts Path objects almost everywhere, including open().
Worked Example · A Safe File Mover
12 minGoal: move a downloaded file into a dated archive folder, renaming it safely. This is the heart of countless real automations.
from pathlib import Path from datetime import date def archive(file_path: str) -> Path: src = Path(file_path) if not src.is_file(): raise FileNotFoundError(f"{src} is not a file") # build target: archive/2026-05-28/<name> folder = Path("archive") / date.today().isoformat() folder.mkdir(parents=True, exist_ok=True) target = folder / src.name # avoid overwriting: report.csv → report-1.csv → report-2.csv counter = 1 while target.exists(): target = folder / f"{src.stem}-{counter}{src.suffix}" counter += 1 src.rename(target) return target moved = archive("downloads/report.csv") print("Moved to", moved)
Moved to archive/2026-05-28/report.csv
Read the code
Every line leans on a pathlib feature: / to build the dated folder, mkdir(parents=True, exist_ok=True) to create it safely, src.name to keep the filename, and src.stem + src.suffix to construct a no-overwrite name (report-1.csv). The same logic in raw strings would be twice as long and break on Windows. This is why pathlib is the backbone of file automation.
Basic
5 minGiven Path("/home/aisha/photos/holiday.jpg"), print its name, stem, suffix, parent, and whether the suffix is an image type. No filesystem needed — paths can be inspected without existing.
Challenge 1
4 minWrite a function that takes a project name and creates name/src, name/tests, and name/README.md (with a title line). Use mkdir(parents=True) and write_text.
Hint
from pathlib import Path def scaffold(name): root = Path(name) (root / "src").mkdir(parents=True, exist_ok=True) (root / "tests").mkdir(exist_ok=True) (root / "README.md").write_text(f"# {name}\n", encoding="utf-8") scaffold("myapp")
Challenge 2
4 minWrite retype(path, new_suffix) that returns a new path with a different extension — report.csv → report.json — using with_suffix. Then make one that swaps the stem with with_name.
Hint
p = Path("report.csv") print(p.with_suffix(".json")) # report.json print(p.with_name("summary.csv")) # summary.csv
Challenge 3 · The Backup Stamp
8 minWrite backup(path) that, given any file, makes a copy beside it named <stem>.bak-<YYYYMMDD-HHMMSS><suffix> using the file's current text content. Return the new path. Verify two backups a second apart don't collide.
Show a sample solution
from pathlib import Path from datetime import datetime def backup(path: str) -> Path: src = Path(path) stamp = datetime.now().strftime("%Y%m%d-%H%M%S") target = src.with_name(f"{src.stem}.bak-{stamp}{src.suffix}") target.write_text(src.read_text(encoding="utf-8"), encoding="utf-8") return target print(backup("note.txt")) # note.bak-20260528-143012.txt
Non-negotiables: uses with_name + stem + suffix, a timestamp, and copies the content.
Recap
3 minpathlib.Path turns paths into objects you build with / and inspect with .name, .stem, .suffix, and .parent. It checks existence (exists, is_file), creates and removes (mkdir, touch, unlink), and reads/writes whole files in one line (read_text, write_text) — all OS-independent. Reach for with_suffix and with_name to derive related paths. From here on, every file automation in this level starts with a Path.
Vocabulary Card
- Path
- An object representing a file or folder location, OS-independent.
- stem / suffix
- The filename without extension, and the extension itself.
- mkdir(parents, exist_ok)
- Create a folder, including missing parents, without erroring if it exists.
- read_text / write_text
- Read or write a whole text file in a single call.
Extra Mission
4 minWrite organise(folder) that looks at every file directly inside a folder and moves it into a subfolder named after its extension (.jpg files → jpg/, .pdf → pdf/, etc.). Create the subfolders as needed and skip files that are already in a subfolder. (Next lesson we'll make it recurse.)
Sample · organise.py
from pathlib import Path def organise(folder: str) -> None: root = Path(folder) for item in root.iterdir(): if item.is_file() and item.suffix: kind = item.suffix.lstrip(".").lower() dest = root / kind dest.mkdir(exist_ok=True) item.rename(dest / item.name) print(f"{item.name} → {kind}/") organise("downloads")
Non-negotiables: iterdir, is_file check, extension-named subfolders via mkdir(exist_ok=True), and rename.