Python pathlib Tutorial: Paths, Read/Write, glob, mkdir and Practical File Scripts

Working with files is one of the first things you do in almost every Python project: open a CSV for a grade list, create a folder for uploads, find every .py file in a project, or build a path that works on both Windows and Linux. For years the usual tools were os.path and plain strings. Today the cleaner option is the built-in pathlib module and its Path objects.

In this Python pathlib tutorial you will learn how to build paths, read and write text and binary files, create folders with mkdir, search with glob and rglob, and apply a few patterns that show up in student and capstone projects. Every script below was run on Python 3.13, and the output under each one is copied from the terminal.

Requirements

  • Python 3.8 or newer (pathlib is in the standard library; nothing to install).
  • A code editor such as VS Code, PyCharm or IDLE.
  • Basic Python: strings, loops and functions.

Download the example zip below, extract it, open a terminal inside the folder, and run each script with python 1_paths.py (or python3 on macOS/Linux).

Why pathlib instead of os.path?

With os.path you glue strings together and hope the slashes are right. Path objects know how to join, resolve and inspect themselves, so the same code works on Windows (\) and Linux/macOS (/). You write Path("data") / "scores.csv" instead of os.path.join("data", "scores.csv"), and methods like .read_text() and .exists() live on the path itself.

Step 1: Build and Inspect Paths

Save this as 1_paths.py:

  1. from pathlib import Path
  2.  
  3. # Current working directory and home
  4. cwd = Path.cwd()
  5. home = Path.home()
  6. print("cwd:", cwd)
  7. print("home:", home)
  8.  
  9. # Build paths without worrying about / vs \
  10. data = Path("sample_data") / "scores.csv"
  11. print("joined:", data)
  12. print("absolute:", data.resolve())
  13. print("name:", data.name)
  14. print("stem:", data.stem)
  15. print("suffix:", data.suffix)
  16. print("parent:", data.parent)
  17.  
  18. # Check existence and type
  19. print("exists?", data.exists())
  20. print("is_file?", data.is_file())
  21. print("is_dir?", Path("sample_data").is_dir())

Run it with python 1_paths.py. Output (your absolute paths will differ):

cwd: /home/you/python-pathlib
home: /home/you
joined: sample_data/scores.csv
absolute: /home/you/python-pathlib/sample_data/scores.csv
name: scores.csv
stem: scores
suffix: .csv
parent: sample_data
exists? True
is_file? True
is_dir? True

A few things to notice:

  • Path.cwd() and Path.home() give you the current directory and the user home folder.
  • The / operator joins path parts. It is the pathlib replacement for os.path.join.
  • .resolve() turns a relative path into an absolute one.
  • .name, .stem and .suffix split scores.csv into the full name, the name without extension, and .csv.
  • .exists(), .is_file() and .is_dir() are the safe checks before you open or delete something.

Step 2: Read and Write Files

You can still use open(), but pathlib wraps the common cases. Save as 2_read_write.py:

  1. from pathlib import Path
  2.  
  3. notes = Path("sample_data") / "notes.txt"
  4.  
  5. # Read whole file as text
  6. text = notes.read_text(encoding="utf-8")
  7. print("raw text:")
  8. print(repr(text))
  9.  
  10. # Write a new file
  11. out = Path("sample_data") / "greeting.txt"
  12. out.write_text("Hello from pathlib!\nLine 2.\n", encoding="utf-8")
  13. print("wrote:", out, "bytes=", out.stat().st_size)
  14.  
  15. # Append by reading then writing (or open in append mode)
  16. extra = out.read_text(encoding="utf-8") + "Appended line.\n"
  17. out.write_text(extra, encoding="utf-8")
  18. print("after append:")
  19. print(out.read_text(encoding="utf-8"), end="")
  20.  
  21. # Binary read/write
  22. blob = Path("sample_data") / "tiny.bin"
  23. blob.write_bytes(b"\x00\x01\x02ABC")
  24. print("binary bytes:", list(blob.read_bytes()))

Output:

raw text:
'line1\nline2\nline3\n'
wrote: sample_data/greeting.txt bytes= 28
after append:
Hello from pathlib!
Line 2.
Appended line.
binary bytes: [0, 1, 2, 65, 66, 67]

Useful methods:

  • read_text(encoding="utf-8") / write_text(...) for text files.
  • read_bytes() / write_bytes(...) for binary data (images, PDFs, small blobs).
  • .stat().st_size for the size in bytes.
  • For large files or line-by-line processing, open the path: with path.open(encoding="utf-8") as f:.

write_text() overwrites the whole file. To append, either read-then-write (as above) or open with mode "a".

Step 3: glob, rglob, mkdir and Walking a Tree

Finding files by pattern is where pathlib really helps. Save as 3_glob_walk.py:

  1. from pathlib import Path
  2.  
  3. root = Path("sample_project")
  4.  
  5. print("All files (non-recursive):")
  6. for p in sorted(root.iterdir()):
  7.     kind = "dir " if p.is_dir() else "file"
  8.     print(f"  [{kind}] {p.name}")
  9.  
  10. print("\n*.py anywhere under sample_project:")
  11. for p in sorted(root.rglob("*.py")):
  12.     print(" ", p)
  13.  
  14. print("\n*.md only in docs/:")
  15. for p in sorted((root / "docs").glob("*.md")):
  16.     print(" ", p)
  17.  
  18. print("\nRecursive walk with rglob('*'):")
  19. for p in sorted(root.rglob("*")):
  20.     if p.is_file():
  21.         print(f"  {p.relative_to(root)} ({p.stat().st_size} bytes)")

Output:

All files (non-recursive):
  [file] README.md
  [dir ] docs
  [dir ] src

*.py anywhere under sample_project:
  sample_project/src/main.py
  sample_project/src/utils.py

*.md only in docs/:
  sample_project/docs/notes.md

Recursive walk with rglob('*'):
  README.md (51 bytes)
  docs/notes.md (8 bytes)
  src/main.py (15 bytes)
  src/utils.py (6 bytes)

Remember the difference:

  • iterdir() lists only the immediate children of a folder.
  • glob("*.md") matches in that folder only.
  • rglob("*.py") (recursive glob) searches every subfolder.

To create folders, use Path("reports").mkdir(parents=True, exist_ok=True). parents=True creates missing parents, and exist_ok=True avoids an error when the folder is already there — the pattern you want in almost every script.

Step 4: Practical Scripts for Capstone Projects

Two patterns show up often: summarizing CSV reports in a folder, and blocking ../ path escapes when a user supplies a relative path. Save as 4_practical.py:

  1. from pathlib import Path
  2. import csv
  3.  
  4. # Capstone-style helper: collect all CSV reports and print row counts
  5. reports_dir = Path("sample_data")
  6. reports_dir.mkdir(exist_ok=True)
  7.  
  8. # Create a second CSV for the demo
  9. more = reports_dir / "attendance.csv"
  10. more.write_text("name,present\nAna,1\nBen,0\nCia,1\n", encoding="utf-8")
  11.  
  12. print("CSV reports found:")
  13. for csv_path in sorted(reports_dir.glob("*.csv")):
  14.     with csv_path.open(newline="", encoding="utf-8") as f:
  15.         rows = list(csv.reader(f))
  16.     # subtract header
  17.     data_rows = max(0, len(rows) - 1)
  18.     print(f"  {csv_path.name}: {data_rows} data rows")
  19.  
  20. # Safe path join: never allow .. escape from a base folder
  21. base = Path("sample_project").resolve()
  22. user_input = "../sample_data/notes.txt"  # malicious-looking relative path
  23. candidate = (base / user_input).resolve()
  24. if base in candidate.parents or candidate == base:
  25.     print("safe to read:", candidate)
  26. else:
  27.     print("blocked path escape:", candidate)
  28.     print("kept inside:", base)

Output (absolute paths shortened):

CSV reports found:
  attendance.csv: 3 data rows
  scores.csv: 0 data rows
blocked path escape: /home/you/python-pathlib/sample_data/notes.txt
kept inside: /home/you/python-pathlib/sample_project

The second half is worth keeping in mind for any upload or "open this file" feature. Resolve both the base folder and the candidate path, then check that the candidate is still inside the base with base in candidate.parents (or candidate == base). If the user typed ../something, the resolved path leaves the base and you refuse it.

Quick Reference

Taskpathlib
Join partsPath("a") / "b" / "c.txt"
Current / home dirPath.cwd(), Path.home()
Read / write textp.read_text(), p.write_text("...")
Make foldersp.mkdir(parents=True, exist_ok=True)
Find filesp.glob("*.csv"), p.rglob("*.py")
Delete filep.unlink() (use missing_ok=True on 3.8+)
Rename / movep.rename(new_path)

Common Mistakes

  • Forgetting encoding="utf-8" when reading text — on some Windows setups the default is not UTF-8.
  • Using str concatenation with + instead of the / operator, which breaks when a slash is missing.
  • Calling mkdir() without exist_ok=True and crashing the second time the script runs.
  • Trusting a user-supplied relative path without resolving it against a fixed base folder.

Conclusion

With pathlib.Path you can replace most of the old os.path string work: build paths with /, read and write with one-liners, create folders safely, and search a project tree with glob and rglob. Practice on the four scripts in the download, then try listing every image in a folder or writing a small backup script that copies *.csv files into a dated directory. Once these feel natural, file handling in your next Python project will be much less noisy.

Download the complete example scripts below and run them on your machine. Happy coding!