104. Pathlib Module
File path manipulation with pathlib.Path
104. Pathlib Module
Rohan’s reports need to live in ~/ram_manager/reports/2026/03/. He starts concatenating strings to build the path and immediately hits the Windows vs. Unix / vs \ problem.
Old way — fragile:
report_dir = home + "/ram_manager/reports/" + year + "/" + month
report_path = report_dir + "/report_" + ts + ".json"New way — clean and cross-platform:
from pathlib import Path
report_dir = Path.home() / "ram_manager" / "reports" / year / month
report_dir.mkdir(parents=True, exist_ok=True) # create all directories, safe to call repeatedly
report_path = report_dir / f"report_{ts}.json"The / operator on Path is overloaded — it joins path components cleanly on any OS.
When finding the latest report:
reports = sorted(report_dir.glob("report_*.json"))
# alphabetical sort = chronological sort because filenames start with YYYYMMDD
latest = reports[-1]
print(latest.name) # "report_20260307_143022.json" (full filename)
print(latest.stem) # "report_20260307_143022" (no extension)
print(latest.suffix) # ".json"
print(latest.parent) # Path(".../reports/2026/03")Why not
os.path?pathlib.Pathis an object — you call methods on it (path.exists(),path.read_text(),path.write_text()).os.pathis a collection of standalone functions that take string arguments. Path is more readable and less error-prone for everything except very old codebases.
💡 Fun fact: pathlib was added in Python 3.4 (2014) via PEP 428. Before it, Python developers used os.path.join() — a function that’s verbose and easy to misuse. The / operator overload on Path objects was inspired by the idea that / is literally the path separator on Unix systems.
⚠️ Watch out: Path.name returns the full filename including the extension (e.g., "notes.pdf"), while Path.stem returns just the filename without the extension (e.g., "notes"). These are easy to mix up. If you need to rename a file and preserve its extension, use path.with_stem("new_name") (Python 3.9+) or path.parent / (new_name + path.suffix).
🤔 Think about it: path.mkdir(parents=True, exist_ok=True) is safe to call repeatedly. What would happen if you called path.mkdir() without exist_ok=True on a directory that already exists? When would you intentionally NOT use exist_ok=True?
Learning objectives
- Join path components using the / operator instead of string concatenation
- Distinguish .name (with extension) from .stem (without extension)
- Access .suffix, .parent, and other Path components
- Use Path objects instead of raw strings for file manipulation
Key concepts
- Path() constructor
- / operator for path joining
- .stem — filename without extension
- .name — full filename with extension
- .suffix — file extension
- .parent — containing directory
Try it
Concept detail
pathlib.Path — Modern File Path Handling
pathlib.Path replaces string-based file path manipulation with an object-oriented interface. The / operator is overloaded to join path components cleanly on Windows (backslash) and Unix (forward slash) alike.
Path Joining
from pathlib import Path
# Old way (fragile):
path = base_dir + "/" + filename # breaks on Windows
# New way (clean):
path = Path(base_dir) / filename # works on all OS
path = Path("/tmp") / "lectures" / "notes.pdf"Path Components
p = Path("/tmp/lectures/week1_notes.pdf")
p.name # "week1_notes.pdf" ← full filename with extension
p.stem # "week1_notes" ← filename WITHOUT extension
p.suffix # ".pdf" ← extension with dot
p.parent # Path("/tmp/lectures")
str(p) # "/tmp/lectures/week1_notes.pdf"Key Attributes and Methods
| Attribute / Method | Returns | Description |
|---|---|---|
.name | str | Full filename including extension |
.stem | str | Filename without extension |
.suffix | str | Extension including dot (e.g. .pdf) |
.parent | Path | Parent directory |
.exists() | bool | True if path exists on disk |
.glob("*.pdf") | iterator | Find matching files |
.read_text() | str | Read file contents |
.write_text(s) | None | Write string to file |
.mkdir(parents=True, exist_ok=True) | None | Create directory safely |
Solution
from pathlib import Path
def build_path(base_dir: str, filename: str) -> Path:
"""Join a base directory and filename into a Path object."""
return Path(base_dir) / filename # / operator for clean path joining
def get_stem(filepath: str) -> str:
"""Return the filename without extension."""
p = Path(filepath)
return p.stem # .stem excludes extension; .name includes it
def get_suffix(filepath: str) -> str:
"""Return the file extension including the dot (e.g. '.pdf')."""
p = Path(filepath)
return p.suffix
def get_parent(filepath: str) -> Path:
"""Return the parent directory as a Path."""
p = Path(filepath)
return p.parent
def path_info(base_dir: str, filename: str) -> dict:
"""Return a dict with path components for a file."""
full = build_path(base_dir, filename)
return {
"full": str(full),
"stem": get_stem(str(full)),
"suffix": get_suffix(str(full)),
"parent": str(get_parent(str(full))),
"name": full.name,
}Tests
def test_build_path_returns_path_object():
result = build_path("/tmp/lectures", "week1.pdf")
assert isinstance(result, Path), f"build_path should return Path, got {type(result)}"
def test_build_path_correct_value():
result = build_path("/tmp/lectures", "week1.pdf")
assert str(result) == "/tmp/lectures/week1.pdf", f"Expected '/tmp/lectures/week1.pdf', got '{result}'"
def test_build_path_no_double_slash():
result = build_path("/tmp/lectures", "notes.txt")
assert "//" not in str(result), f"Path should not have double slashes: '{result}'"
def test_get_stem_no_extension():
result = get_stem("/tmp/lectures/week1_notes.pdf")
assert result == "week1_notes", f"Expected 'week1_notes', got '{result}'"
def test_get_stem_not_same_as_name():
# .name would return "week1_notes.pdf" — stem must NOT include extension
result = get_stem("/tmp/lectures/week1_notes.pdf")
assert ".pdf" not in result, f"stem should not include extension, got '{result}'"
def test_get_suffix_includes_dot():
result = get_suffix("/tmp/lectures/week1_notes.pdf")
assert result == ".pdf", f"Expected '.pdf', got '{result}'"
def test_get_parent_correct():
result = get_parent("/tmp/lectures/week1.pdf")
assert str(result) == "/tmp/lectures", f"Expected '/tmp/lectures', got '{result}'"
def test_path_info_full_breakdown():
info = path_info("/tmp/lectures", "intro_to_python.pdf")
assert info["stem"] == "intro_to_python"
assert info["suffix"] == ".pdf"
assert info["parent"] == "/tmp/lectures"
assert info["name"] == "intro_to_python.pdf"
assert info["full"] == "/tmp/lectures/intro_to_python.pdf"