Python · Syntax · Intermediate

pathlib

10 tasks

Learn to work with file system paths using Python's modern pathlib module — cleaner and more powerful than os.path.

Path Objects & Navigation

#
`pathlib` provides the `Path` class — an object-oriented way to work with filesystem paths. It replaces `os.path` string manipulation with clean, readable attribute access. ```python from pathlib import Path ``` ## Creating Path objects ```python p = Path("/home/user/documents/report.txt") p = Path(".") # current directory p = Path.home() # home directory: /home/user p = Path.cwd() # current working directory # Windows paths also work: p = Path("C:/Users/Alice/Desktop/file.txt") ``` ## Path attributes ```python p = Path("/home/user/projects/script.py") print(p.name) # "script.py" — filename with extension print(p.stem) # "script" — filename without extension print(p.suffix) # ".py" — extension including dot print(p.suffixes) # [".py"] — all extensions (for ".tar.gz" gives [".tar", ".gz"]) print(p.parent) # /home/user/projects — parent directory print(p.parents) # all parent dirs (iterable) print(p.parts) # ("/", "home", "user", "projects", "script.py") print(p.root) # "/" print(p.anchor) # "/" — drive + root on Windows ``` ## Joining paths Use `/` operator — far cleaner than `os.path.join`: ```python base = Path("/home/user") config = base / "config" / "settings.json" print(config) # /home/user/config/settings.json # Works with strings on the right side reports = base / "reports" report_file = reports / "2024" / "q1.csv" ``` ## Checking existence ```python p = Path("/home/user/data.txt") print(p.exists()) # True/False — file or dir exists print(p.is_file()) # True if it exists and is a regular file print(p.is_dir()) # True if it exists and is a directory print(p.is_symlink()) # True if it is a symbolic link ``` ## Converting and comparing ```python p = Path("/home/user/file.txt") # Convert to string str(p) # "/home/user/file.txt" p.as_posix() # "/home/user/file.txt" — always forward slashes # Absolute path p.resolve() # resolves symlinks, returns absolute Path # Relative path p.relative_to("/home/user") # Path("file.txt") ```

Reading, Writing & Searching Files

#
## Reading and writing files `Path` objects can read and write text and bytes directly — no `open()` needed for simple cases: ```python from pathlib import Path p = Path("notes.txt") # Write text p.write_text("Hello, pathlib!\n", encoding="utf-8") # Read text content = p.read_text(encoding="utf-8") print(content) # Hello, pathlib! # Read bytes data = p.read_bytes() # Write bytes p.write_bytes(b"\x00\x01\x02") ``` For line-by-line reading, use `open()` with a Path: ```python with p.open("r", encoding="utf-8") as f: for line in f: print(line.strip()) ``` ## Listing directory contents ```python d = Path("/home/user/projects") # All entries (files and subdirs) for item in d.iterdir(): print(item.name, "dir" if item.is_dir() else "file") # Only files files = [f for f in d.iterdir() if f.is_file()] # Only directories subdirs = [f for f in d.iterdir() if f.is_dir()] ``` ## glob: pattern-based search ```python src = Path("/home/user/projects") # All Python files in the directory (non-recursive) py_files = list(src.glob("*.py")) # All Python files recursively (** = any number of dirs) all_py = list(src.rglob("*.py")) # All log files in any subdirectory named "logs" logs = list(src.glob("**/logs/*.log")) ``` ## Creating directories ```python new_dir = Path("/home/user/projects/new_feature") # Create one directory new_dir.mkdir() # Create parent directories if they don't exist, no error if already exists new_dir.mkdir(parents=True, exist_ok=True) ``` ## Renaming and deleting ```python p = Path("old_name.txt") # Rename (returns new Path) new_p = p.rename("new_name.txt") print(new_p) # new_name.txt # Delete a file p.unlink() # Delete an empty directory d = Path("empty_dir") d.rmdir() # Check and delete if p.exists(): p.unlink() # Python 3.8+: missing_ok p.unlink(missing_ok=True) # no error if not found ``` ## Practical example: process all CSV files ```python data_dir = Path("data") output_dir = Path("output") output_dir.mkdir(exist_ok=True) for csv_file in data_dir.glob("*.csv"): content = csv_file.read_text() processed = content.upper() # dummy transformation out_file = output_dir / csv_file.name out_file.write_text(processed) print(f"Processed: {csv_file.name}") ```

pathlib Reference

#
## Path attributes (read-only) | Attribute | Example value | What it returns | |---|---|---| | `p.name` | `"script.py"` | Filename with extension | | `p.stem` | `"script"` | Filename without extension | | `p.suffix` | `".py"` | Last extension (with dot) | | `p.suffixes` | `[".tar", ".gz"]` | All extensions | | `p.parent` | `Path("/home/user")` | Parent directory Path | | `p.parents` | (iterable) | All ancestor directories | | `p.parts` | `("/", "home", "user", "f.py")` | Path components tuple | | `p.root` | `"/"` | Root component | | `p.anchor` | `"/"` or `"C:\"` | Drive + root | ## Path methods — querying | Method | What it does | |---|---| | `p.exists()` | True if path exists (file or dir) | | `p.is_file()` | True if exists and is a regular file | | `p.is_dir()` | True if exists and is a directory | | `p.is_symlink()` | True if symbolic link | | `p.stat()` | os.stat_result (size, mtime, etc.) | | `p.resolve()` | Absolute path with symlinks resolved | | `p.relative_to(other)` | Relative path from `other` | | `p.with_name(name)` | New path with different name | | `p.with_stem(stem)` | New path with different stem | | `p.with_suffix(suf)` | New path with different extension | ## Path methods — file I/O | Method | What it does | |---|---| | `p.read_text(encoding=...)` | Read content as str | | `p.write_text(s, encoding=...)` | Write str, return char count | | `p.read_bytes()` | Read content as bytes | | `p.write_bytes(b)` | Write bytes, return byte count | | `p.open(mode, encoding=...)` | Open file object (context manager) | ## Path methods — filesystem operations | Method | What it does | |---|---| | `p.mkdir(parents=False, exist_ok=False)` | Create directory | | `p.rename(target)` | Rename, return new Path | | `p.replace(target)` | Rename, overwrite if exists | | `p.unlink(missing_ok=False)` | Delete file or symlink | | `p.rmdir()` | Delete empty directory | | `p.touch()` | Create empty file or update mtime | | `p.iterdir()` | Iterate over directory contents | | `p.glob(pattern)` | Match files with pattern | | `p.rglob(pattern)` | Recursive glob | ## Glob patterns | Pattern | Matches | |---|---| | `*` | Any name in current dir | | `*.py` | Python files in current dir | | `**/*.py` | Python files recursively | | `??.txt` | Two-character name + .txt | | `[abc].txt` | a.txt, b.txt, or c.txt | ## pathlib vs os.path ```python # Old way (os.path) import os full = os.path.join(base, "subdir", "file.txt") name = os.path.basename(full) ext = os.path.splitext(name)[1] # Modern way (pathlib) from pathlib import Path full = Path(base) / "subdir" / "file.txt" name = full.name ext = full.suffix ```
01

Path Attributes

#

Given the path string `"/home/user/projects/data/report_2024.csv"`, create a `Path` object from it and print: - The full filename (`name`) - The filename without extension (`stem`) - The extension (`suffix`) - The parent directory (`parent`) - All parts of the path (`parts`)

from pathlib import Path

p = Path("/home/user/projects/data/report_2024.csv")

# Print: name, stem, suffix, parent, parts
print(p.name)    # ???
print(p.stem)    # ???
print(p.suffix)  # ???
print(p.parent)  # ???
print(p.parts)   # ???
Solution
from pathlib import Path

p = Path("/home/user/projects/data/report_2024.csv")

print(p.name)    # report_2024.csv
print(p.stem)    # report_2024
print(p.suffix)  # .csv
print(p.parent)  # /home/user/projects/data
print(p.parts)   # ("/", "home", "user", "projects", "data", "report_2024.csv")
02

Build Path with /

#

Write a function `build_path(base, *parts)` that takes a base directory as a string and any number of path parts, joins them using the `/` operator, and returns the final `Path` object. Example: `build_path("/home/user", "projects", "myapp", "config.json")` → `Path("/home/user/projects/myapp/config.json")`.

from pathlib import Path


def build_path(base, *parts):
    pass


result = build_path("/home/user", "projects", "myapp", "config.json")
print(result)   # /home/user/projects/myapp/config.json
Solution
from pathlib import Path


def build_path(base, *parts):
    p = Path(base)
    for part in parts:
        p = p / part
    return p


# Shorter: Path(base).joinpath(*parts)
result = build_path("/home/user", "projects", "myapp", "config.json")
print(result)   # /home/user/projects/myapp/config.json
03

Change Extension

#

Write a function `change_extension(path_str, new_ext)` that takes a path string and a new extension (with dot, like `".txt"`), and returns a new `Path` with the extension changed. Example: `change_extension("/home/user/data.csv", ".json")` → `Path("/home/user/data.json")`. Use `p.with_suffix()`.

from pathlib import Path


def change_extension(path_str, new_ext):
    pass


print(change_extension("/home/user/data.csv", ".json"))
# /home/user/data.json
print(change_extension("/docs/report.txt", ".md"))
# /docs/report.md
Solution
from pathlib import Path


def change_extension(path_str, new_ext):
    return Path(path_str).with_suffix(new_ext)


print(change_extension("/home/user/data.csv", ".json"))
# /home/user/data.json
print(change_extension("/docs/report.txt", ".md"))
# /docs/report.md
04

Safe Read Text

#

Write a function `safe_read(path_str, encoding="utf-8")` that reads the content of a file at `path_str` and returns it as a string. If the file does not exist, return `None`. If any other error occurs, return `None`. Use `Path.read_text()`.

from pathlib import Path


def safe_read(path_str, encoding="utf-8"):
    pass


Path("/tmp/test_pathlib.txt").write_text("hello", encoding="utf-8")
print(safe_read("/tmp/test_pathlib.txt"))   # hello
print(safe_read("/tmp/missing.txt"))        # None
Solution
from pathlib import Path


def safe_read(path_str, encoding="utf-8"):
    try:
        return Path(path_str).read_text(encoding=encoding)
    except Exception:
        return None


Path("/tmp/test_pathlib.txt").write_text("hello", encoding="utf-8")
print(safe_read("/tmp/test_pathlib.txt"))   # hello
print(safe_read("/tmp/missing.txt"))        # None
05

Write and Read Back

#

Write a function `write_and_verify(path_str, content)` that: 1. Writes `content` to the file at `path_str` using `Path.write_text()` 2. Reads it back using `Path.read_text()` 3. Returns `True` if the content matches, `False` otherwise Use `encoding="utf-8"` for both operations.

from pathlib import Path


def write_and_verify(path_str, content):
    pass


print(write_and_verify("/tmp/verify_test.txt", "Hello pathlib!"))
# True
Solution
from pathlib import Path


def write_and_verify(path_str, content):
    p = Path(path_str)
    p.write_text(content, encoding="utf-8")
    return p.read_text(encoding="utf-8") == content


print(write_and_verify("/tmp/verify_test.txt", "Hello pathlib!"))
# True
06

Count Files by Extension

#

Write a function `count_by_extension(dir_path)` that takes a directory path string and returns a dictionary mapping each file extension to the count of files with that extension in the directory (non-recursive). Example: if the dir has `a.py`, `b.py`, `c.txt`, `d.txt`, `e.md`, result is `{".py": 2, ".txt": 2, ".md": 1}`.

from pathlib import Path


def count_by_extension(dir_path):
    pass


# Example: if dir has a.py, b.py, c.txt, d.txt, e.md
# result: {".py": 2, ".txt": 2, ".md": 1}
Solution
from pathlib import Path


def count_by_extension(dir_path):
    counts = {}
    for p in Path(dir_path).iterdir():
        if p.is_file():
            ext = p.suffix
            counts[ext] = counts.get(ext, 0) + 1
    return counts


# Test with a temp directory
import tempfile, os
with tempfile.TemporaryDirectory() as d:
    for name in ["a.py", "b.py", "c.txt", "d.txt", "e.md"]:
        Path(d, name).write_text("", encoding="utf-8")
    print(count_by_extension(d))
    # {".py": 2, ".txt": 2, ".md": 1}
07

Find Large Files

#

Write a function `find_large_files(dir_path, min_size_bytes)` that recursively searches a directory for all files larger than `min_size_bytes` and returns their paths as a sorted list of `Path` objects. Use `p.rglob("*")` and `p.stat().st_size`.

from pathlib import Path


def find_large_files(dir_path, min_size_bytes):
    pass


# Returns sorted list of Path objects for files > min_size_bytes
Solution
from pathlib import Path


def find_large_files(dir_path, min_size_bytes):
    return sorted(
        p for p in Path(dir_path).rglob("*")
        if p.is_file() and p.stat().st_size > min_size_bytes
    )


# Test
for f in find_large_files(".", 1000):
    print(f)
08

Ensure Directory

#

Write a function `ensure_dir(path_str)` that creates the directory at `path_str` if it does not exist. It should: - Create all intermediate parent directories as needed - Not raise an error if the directory already exists - Return the `Path` object of the directory Use `Path.mkdir(parents=True, exist_ok=True)`.

from pathlib import Path


def ensure_dir(path_str):
    pass


p = ensure_dir("/tmp/myapp/logs/2024")
print(p.is_dir())   # True
print(p)            # /tmp/myapp/logs/2024
Solution
from pathlib import Path


def ensure_dir(path_str):
    p = Path(path_str)
    p.mkdir(parents=True, exist_ok=True)
    return p


p = ensure_dir("/tmp/myapp/logs/2024")
print(p.is_dir())   # True
print(p)            # /tmp/myapp/logs/2024
09

Backup File

#

Write a function `backup(path_str)` that creates a backup copy of a file by appending `.bak` to its name. If the file does not exist, raise `FileNotFoundError`. Return the `Path` of the backup file. Example: `backup("/home/user/data.txt")` creates `/home/user/data.txt.bak` and returns that path.

from pathlib import Path
import shutil


def backup(path_str):
    pass


Path("/tmp/data_original.txt").write_text("important", encoding="utf-8")
bak = backup("/tmp/data_original.txt")
print(bak)   # /tmp/data_original.txt.bak
Solution
from pathlib import Path
import shutil


def backup(path_str):
    p = Path(path_str)
    if not p.exists():
        raise FileNotFoundError(f"File not found: {p}")
    bak = p.with_suffix(p.suffix + ".bak")
    shutil.copy2(str(p), str(bak))
    return bak


Path("/tmp/data_original.txt").write_text("important", encoding="utf-8")
bak = backup("/tmp/data_original.txt")
print(bak)   # /tmp/data_original.txt.bak
print(bak.read_text(encoding="utf-8"))   # important
10

Find Python Files

#

Write a function `find_python_files(start_dir)` that recursively finds all `.py` files under `start_dir`, sorts them by name, and returns a list of their relative paths (relative to `start_dir`) as strings. Use `Path.rglob("*.py")` and `p.relative_to(start_dir)`.

from pathlib import Path


def find_python_files(start_dir):
    pass


# Returns list of relative path strings sorted by name
for f in find_python_files("."):
    print(f)
Solution
from pathlib import Path


def find_python_files(start_dir):
    base = Path(start_dir)
    return sorted(
        str(p.relative_to(base))
        for p in base.rglob("*.py")
    )


for f in find_python_files("."):
    print(f)