← Home

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.Path is an object — you call methods on it (path.exists(), path.read_text(), path.write_text()). os.path is 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 / MethodReturnsDescription
.namestrFull filename including extension
.stemstrFilename without extension
.suffixstrExtension including dot (e.g. .pdf)
.parentPathParent directory
.exists()boolTrue if path exists on disk
.glob("*.pdf")iteratorFind matching files
.read_text()strRead file contents
.write_text(s)NoneWrite string to file
.mkdir(parents=True, exist_ok=True)NoneCreate 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"

Resources