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.
Try It Yourself
13 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.
Write 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")
Write 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
Mini-Challenge · 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.
Homework
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.