MidТеория6 min

pathlib

Объектно-ориентированная работа с путями: Path, glob, чтение/запись и сравнение с os.path

Модуль pathlib предоставляет объектно-ориентированный API для работы с путями файловой системы. Он появился в Python 3.4 и стал стандартом де-факто, заменив процедурный os.path. Пути в pathlib -- это объекты с методами, а не строки с функциями.

Создание путей

from pathlib import Path

# Current directory
cwd = Path.cwd()
print(cwd)  # /Users/developer/project

# Home directory
home = Path.home()
print(home)  # /Users/developer

# From string
config = Path("/etc/nginx/nginx.conf")

# From parts
project = Path("src") / "mypackage" / "main.py"
print(project)  # src/mypackage/main.py

# Operator / builds paths naturally
data_dir = Path.home() / "Documents" / "data"
print(data_dir)  # /Users/developer/Documents/data

Свойства пути

from pathlib import Path

path = Path("/Users/developer/project/src/main.py")

print(path.name)       # main.py — filename with extension
print(path.stem)       # main — filename without extension
print(path.suffix)     # .py — file extension
print(path.suffixes)   # ['.py'] — all extensions
print(path.parent)     # /Users/developer/project/src
print(path.parents[0]) # /Users/developer/project/src
print(path.parents[1]) # /Users/developer/project
print(path.parts)      # ('/', 'Users', 'developer', 'project', 'src', 'main.py')

# Multiple extensions
archive = Path("data.tar.gz")
print(archive.suffixes)  # ['.tar', '.gz']
print(archive.stem)      # data.tar
from pathlib import Path

path = Path("/project/src/main.py")

# Change filename
new_path = path.with_name("app.py")
print(new_path)  # /project/src/app.py

# Change extension
test_path = path.with_suffix(".txt")
print(test_path)  # /project/src/main.txt

# Change stem (Python 3.12+)
renamed = path.with_stem("module")
print(renamed)  # /project/src/module.py

# Navigate up
parent = path.parent
print(parent)  # /project/src

grandparent = path.parent.parent
print(grandparent)  # /project

# Join paths
new_file = path.parent / "utils" / "helpers.py"
print(new_file)  # /project/src/utils/helpers.py

Проверка существования и типа

from pathlib import Path

path = Path("pyproject.toml")

# Existence checks
print(path.exists())      # True/False
print(path.is_file())     # True if it's a file
print(path.is_dir())      # True if it's a directory
print(path.is_symlink())  # True if it's a symbolic link
print(path.is_absolute()) # True if path is absolute

# File info
if path.exists():
    stat = path.stat()
    print(f"Size: {stat.st_size} bytes")
    print(f"Modified: {stat.st_mtime}")

    # Human-readable size
    from datetime import datetime
    modified = datetime.fromtimestamp(stat.st_mtime)
    print(f"Last modified: {modified}")

Чтение и запись файлов

pathlib предоставляет удобные однострочники для простых операций:

from pathlib import Path

config = Path("config.txt")

# Write text
config.write_text("host=localhost\nport=5432\n", encoding="utf-8")

# Read text
content = config.read_text(encoding="utf-8")
print(content)

# Write bytes
binary_path = Path("data.bin")
binary_path.write_bytes(b"\x89PNG\r\n\x1a\n")

# Read bytes
data = binary_path.read_bytes()
print(data[:4])  # b'\x89PNG'

Работа со строками

from pathlib import Path

# Read lines
lines = Path("data.txt").read_text(encoding="utf-8").splitlines()
for line in lines:
    print(line)

# Write lines
output = Path("output.txt")
output.write_text("\n".join(["line 1", "line 2", "line 3"]), encoding="utf-8")

Сочетание с open()

Для более сложных операций используйте Path.open():

from pathlib import Path

log_path = Path("app.log")

# Path.open() works like built-in open()
with log_path.open("a", encoding="utf-8") as f:
    f.write("New log entry\n")

# Read large file line by line
with log_path.open(encoding="utf-8") as f:
    for line in f:
        if "ERROR" in line:
            print(line.strip())

Поиск файлов: glob и rglob

from pathlib import Path

project = Path("src")

# Find all Python files in directory
for py_file in project.glob("*.py"):
    print(py_file)

# Recursive search — all Python files in all subdirectories
for py_file in project.rglob("*.py"):
    print(py_file)

# Pattern matching
for config in Path("/etc").glob("*.conf"):
    print(config)

# Multiple patterns
from itertools import chain
patterns = ["*.py", "*.toml", "*.yml"]
all_files = chain.from_iterable(project.rglob(p) for p in patterns)

# Complex patterns
for test_file in project.rglob("test_*.py"):
    print(f"Test: {test_file}")

Фильтрация результатов

from pathlib import Path

src = Path("src")

# Only files (not directories)
python_files = [p for p in src.rglob("*.py") if p.is_file()]

# Files larger than 1KB
large_files = [
    p for p in src.rglob("*")
    if p.is_file() and p.stat().st_size > 1024
]

# Sort by modification time
recent_files = sorted(
    src.rglob("*.py"),
    key=lambda p: p.stat().st_mtime,
    reverse=True
)[:5]  # 5 most recently modified

Создание и удаление

from pathlib import Path

# Create directory (and parents if needed)
data_dir = Path("data") / "processed" / "2024"
data_dir.mkdir(parents=True, exist_ok=True)

# Create a file
file_path = data_dir / "results.txt"
file_path.touch()  # Create empty file (or update modification time)

# Delete a file
file_path.unlink()

# Delete file if exists (Python 3.8+)
file_path.unlink(missing_ok=True)

# Delete empty directory
data_dir.rmdir()

# Delete directory tree — use shutil
import shutil
shutil.rmtree(Path("data"))

Переименование и перемещение

from pathlib import Path

source = Path("old_name.txt")
source.write_text("content", encoding="utf-8")

# Rename (same directory)
new_path = source.rename("new_name.txt")
print(new_path)  # new_name.txt

# Move to another directory
dest = Path("archive") / "new_name.txt"
Path("archive").mkdir(exist_ok=True)
moved = new_path.rename(dest)

# Replace (overwrite if exists)
backup = Path("backup.txt")
backup.write_text("old backup", encoding="utf-8")
dest.replace(backup)  # Overwrites backup.txt

Сравнение: os.path vs pathlib

import os
from pathlib import Path

# --- Joining paths ---
# os.path
result = os.path.join("/home", "user", "docs", "file.txt")

# pathlib
result = Path("/home") / "user" / "docs" / "file.txt"

# --- Getting filename ---
# os.path
name = os.path.basename("/home/user/docs/file.txt")

# pathlib
name = Path("/home/user/docs/file.txt").name

# --- Checking existence ---
# os.path
exists = os.path.exists("/home/user/docs")

# pathlib
exists = Path("/home/user/docs").exists()

# --- Reading a file ---
# os.path style
with open(os.path.join("data", "config.txt")) as f:
    content = f.read()

# pathlib style
content = (Path("data") / "config.txt").read_text(encoding="utf-8")

# --- Walking directories ---
# os.walk
for root, dirs, files in os.walk("src"):
    for file in files:
        filepath = os.path.join(root, file)
        print(filepath)

# pathlib
for filepath in Path("src").rglob("*"):
    if filepath.is_file():
        print(filepath)

# --- Getting extension ---
# os.path
_, ext = os.path.splitext("archive.tar.gz")  # '.gz'

# pathlib
ext = Path("archive.tar.gz").suffix           # '.gz'
exts = Path("archive.tar.gz").suffixes        # ['.tar', '.gz']

Практический пример: организатор файлов

from pathlib import Path
from collections import defaultdict

def organize_by_extension(source_dir: str, target_dir: str) -> dict[str, int]:
    """Organize files into subdirectories by extension."""
    source = Path(source_dir)
    target = Path(target_dir)
    stats: dict[str, int] = defaultdict(int)

    for file_path in source.iterdir():
        if not file_path.is_file():
            continue

        # Determine target subdirectory
        ext = file_path.suffix.lower().lstrip(".")
        if not ext:
            ext = "no_extension"

        # Create subdirectory and move file
        ext_dir = target / ext
        ext_dir.mkdir(parents=True, exist_ok=True)

        dest = ext_dir / file_path.name
        file_path.rename(dest)
        stats[ext] += 1

    return dict(stats)

def find_duplicates(directory: str) -> dict[int, list[Path]]:
    """Find files with the same size (potential duplicates)."""
    size_map: dict[int, list[Path]] = defaultdict(list)

    for file_path in Path(directory).rglob("*"):
        if file_path.is_file():
            size = file_path.stat().st_size
            size_map[size].append(file_path)

    # Return only groups with duplicates
    return {size: paths for size, paths in size_map.items() if len(paths) > 1}

Проверь себя

Что возвращает Path.stem для файла 'archive.tar.gz'?

Чем rglob отличается от glob?

Как создать директорию со всеми промежуточными папками?

Какой метод Path читает текстовый файл целиком и возвращает строку?

Как в pathlib объединить части пути?