Learning Platform
Урок 05.04 · 16 мин
Начальный
pathlibFile systemFilesystem pathsI/O
Партиционирование: Hive-конвенция в data lake pathlib: продвинутые сценарии

Почему именно pathlib

В Python для работы с путями есть два пути: старый os.path (строковый, из 90-х) и современный pathlib (объектный, с Python 3.4). На большинстве работ в 2026 году вы увидите второй — он короче, безопаснее и кросс-платформенный из коробки. Если в чужом легаси-коде встретите os.path.join(...) — это нормально, но в свой новый код пишите pathlib.

Главная идея: путь — это не просто строка, у него есть структура. pathlib.Path возвращает объект, у которого есть методы exists(), is_file(), parent, name, suffix. Все эти операции, конечно, можно делать и через os.path, но там каждая — отдельная функция, и читается это как набор if-else по странным аргументам.

Сравним один и тот же код в обоих стилях:

# os.path — старая школа
import os
import os.path

base = "/data/raw"
filename = "events.csv"
full_path = os.path.join(base, "2026", "05", filename)
if not os.path.exists(os.path.dirname(full_path)):
    os.makedirs(os.path.dirname(full_path))
with open(full_path) as f:
    text = f.read()
# pathlib — современный стиль
from pathlib import Path

full_path = Path("/data/raw") / "2026" / "05" / "events.csv"
full_path.parent.mkdir(parents=True, exist_ok=True)
text = full_path.read_text()

Второй вариант на четверть короче, читается линейно сверху вниз и не содержит ни одной строки в os.path.something(...). Все операции — методы на объекте.

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

Path принимает строку (или несколько частей) и возвращает объект:

from pathlib import Path

p = Path("/data/orders.csv")              # абсолютный
rel = Path("data") / "orders.csv"         # относительный, через оператор /
cwd = Path.cwd()                          # текущая рабочая директория
home = Path.home()                        # домашняя папка пользователя

Оператор /

переопределён
для конкатенации путей. Он автоматически выбирает разделитель — / на Linux/macOS, \ на Windows. Сам разделитель в коде писать никогда не нужно.

Path("data") / "orders.csv" работает потому, что у класса Path определён метод __truediv__. Слева может быть Path, справа — Path или строка. Результат — новый Path. Это идиоматично и читается как естественный URL/путь.

Проверки и метаданные

Самый частый набор операций — проверить, существует ли путь, что это (файл, директория) и какого размера:

p = Path("/data/orders.csv")

p.exists()       # True/False — есть ли что-то по этому пути
p.is_file()      # это файл?
p.is_dir()       # это директория?
p.is_symlink()   # это символическая ссылка?

p.stat().st_size   # размер в байтах
p.stat().st_mtime  # время последней модификации, unix-timestamp

stat() возвращает объект os.stat_result со всеми метаданными — тот же, что os.stat, просто доступен как метод объекта.

Часто нужная штука — части пути:

p = Path("/data/raw/2026/05/orders.csv")

p.name         # 'orders.csv'      — последний компонент
p.stem         # 'orders'           — имя без расширения
p.suffix       # '.csv'             — расширение, включая точку
p.parent       # Path('/data/raw/2026/05')
p.parents[2]   # Path('/data/raw')  — два уровня вверх
p.parts        # ('/', 'data', 'raw', '2026', '05', 'orders.csv')

И симметричные операции — построить путь, изменив одну деталь:

p = Path("/data/orders.csv")
p.with_suffix(".parquet")       # Path('/data/orders.parquet')
p.with_name("customers.csv")    # Path('/data/customers.csv')
p.with_stem("orders_v2")        # Path('/data/orders_v2.csv')

Это особенно полезно в ETL: «возьми все .csv в папке, конвертируй в .parquet с тем же именем». Никаких ручных манипуляций со строками.

Создание, удаление, переименование

p = Path("/data/raw/2026/05")

# создать директорию; parents=True — создать промежуточные; exist_ok=True — не падать, если уже есть
p.mkdir(parents=True, exist_ok=True)

# удалить файл (для директорий — rmdir(), но только если пустая)
Path("/tmp/old.log").unlink(missing_ok=True)

# переименовать/переместить файл
src = Path("/data/raw/orders.csv")
src.rename("/data/processed/orders.csv")

Аргументы parents=True и exist_ok=True — критичны в production-коде. Без них первый запуск ETL упадёт на «директория не существует», а второй — на «директория уже есть». С ними — обе ситуации обработаны идемпотентно.

Обход директорий

Три инструмента для перебора файлов внутри папки.

iterdir() — итератор по элементам директории первого уровня:

for child in Path("/data/raw").iterdir():
    print(child)   # все файлы и поддиректории в /data/raw

glob(pattern) — поиск по wildcard в текущей директории:

for csv_file in Path("/data/raw").glob("*.csv"):
    print(csv_file)

rglob(pattern) — рекурсивный поиск по всем подпапкам:

for csv_file in Path("/data/raw").rglob("*.csv"):
    print(csv_file)
# найдёт data/raw/2026/05/a.csv, data/raw/2026/06/b.csv, и т.д.

В шаблонах работают стандартные wildcards: * (любые символы), ? (один символ), [abc] (один из символов). ** — означает «любая глубина», но используется только с rglob или в glob через **/.

Все три метода возвращают итераторы, не списки. Это важно, если в папке миллионы файлов — память расходоваться не будет. Хотите сразу список — оборачивайте в list(...).

Обход data-папки: iterdir vs glob vs rglob

Три разные стратегии: один уровень, шаблон, рекурсивно

iterdir()один уровень
видитraw/2026, raw/lookup.csv
не видитraw/2026/05/a.csvГлубже не идёт
glob('*.csv')один уровень, шаблон
видитraw/lookup.csv
не видитraw/2026/05/a.csv* — это в текущей директории
rglob('*.csv')всё дерево
видитraw/lookup.csv, raw/2026/05/a.csv
есть стоимостьO(всех файлов в дереве)Тяжёлая операция в больших папках

Простое I/O

pathlib облегчает разовое чтение/запись маленьких файлов — не нужны контекст-менеджеры:

text = Path("config.toml").read_text()
data = Path("image.png").read_bytes()

Path("output.json").write_text('{"ok": true}')
Path("dump.bin").write_bytes(b"\x00\x01\x02")

Эти методы открывают файл, читают/пишут целиком и закрывают. Удобно для маленьких файлов: TOML, JSON, манифесты. Для огромных CSV — всё равно нужен with open(...) со стримингом (см. урок 01 про генераторы).

read_text принимает параметры encoding/errors, но в Python 3.13 кодировка по умолчанию — utf-8, и это правильно. Если работаете с legacy-файлами в cp1251/latin-1 — указывайте явно: read_text(encoding="cp1251").

DE-кейс: партиционирование вывода по дате

Типичная задача — ETL пишет результат не одним файлом, а размазывает по папкам по дате обработки. Это нужно, чтобы потом легко читать только нужный период (например, в Spark или из аналитического запроса). Стандартная схема — data/{year}/{month}/{day}/file.parquet.

from datetime import date
from pathlib import Path

def partition_path(base: Path, when: date, name: str) -> Path:
    """Вернуть путь data/YYYY/MM/DD/name, создав папки если их нет."""
    parts = base / f"{when.year:04d}" / f"{when.month:02d}" / f"{when.day:02d}"
    parts.mkdir(parents=True, exist_ok=True)
    return parts / name

# использование
out = partition_path(Path("/data/processed"), date.today(), "orders.parquet")
out.write_bytes(arrow_table.to_pandas().to_parquet())

Несколько важных деталей. Форматирование через f"{when.year:04d}" — это лидирующие нули: 2026 будет «2026», 25-й апрель — «04», не «4». В путях это критично: «09» сортируется в одну позицию с «09», а «9» — в другую. Партиции по 2026/05/09 корректно сортируются строкой, по 2026/5/9 — нет.

Этот же паттерн под названием Hive-partitioning используется во всех современных дата-системах: Spark, Polars, DuckDB, Athena. Запомните его как стандартный.

DE-кейс: обход data-папки

Допустим, прилетела папка с сотнями CSV — нужно прочитать первую строку (заголовок) каждого, чтобы понять схему. Pathlib делает это компактно:

import csv
from pathlib import Path

def collect_headers(data_dir: Path) -> dict[str, list[str]]:
    headers: dict[str, list[str]] = {}
    for path in data_dir.rglob("*.csv"):
        with path.open(encoding="utf-8") as f:
            reader = csv.reader(f)
            headers[path.name] = next(reader)
    return headers

for name, cols in collect_headers(Path("/data/raw")).items():
    print(name, cols)

Метод path.open(...) — то же, что open(path, ...), только синтаксически объектный. Принимает те же аргументы (encoding, mode, newline).

pathlib против os.path: короткая сводка

Задачаos.pathpathlib
Склейка путейos.path.join(a, b, c)Path(a) / b / c
Существованиеos.path.exists(p)p.exists()
Файл лиos.path.isfile(p)p.is_file()
Расширениеos.path.splitext(p)[1]p.suffix
Родительos.path.dirname(p)p.parent
Имя файлаos.path.basename(p)p.name
Создать дир.os.makedirs(p, exist_ok=True)p.mkdir(parents=True, exist_ok=True)
Глобglob.glob('**/*.csv', recursive=True)Path('.').rglob('*.csv')

Все операции делают одно и то же, но pathlib читается линейно и не требует помнить разные имена функций. Один объект — все методы на нём.

Подводные камни

Path неизменяем. Любая операция вроде with_suffix возвращает новый объект, исходный остаётся. Если вы пишете p.with_suffix('.parquet') и не сохраняете результат — операция бессмысленна.

Сравнение путей. Path('/a/b') == Path('/a/b') — True, но Path('/a/b') == Path('/a/b/') — это тоже True (в современных версиях), а Path('/a/./b') — False (потому что не нормализован). Если нужна канонизация — вызывайте .resolve(), он разворачивает все символические ссылки и ...

Не передавайте Path туда, где ожидается строка, без проверки. Большинство stdlib- и third-party-функций принимают Path начиная с Python 3.6+, но иногда в legacy-коде встречается if isinstance(x, str) — оборачивайте str(path) или меняйте сторонний код.

.glob() возвращает в произвольном порядке. Файловая система не обязана отдавать алфавитный список. Если порядок важен — сортируйте: sorted(Path('.').glob('*.csv')).

Что мы получили

  • pathlib.Path — современная замена os.path. Один объект — все нужные методы.
  • Оператор / склеивает пути с правильным разделителем для текущей ОС.
  • mkdir(parents=True, exist_ok=True) — идиоматический способ обеспечить наличие папки.
  • glob, rglob, iterdir — три уровня обхода: шаблон в текущем уровне, шаблон рекурсивно, всё первого уровня.
  • read_text / write_text — для маленьких файлов; для больших по-прежнему with path.open().
  • DE-применение: партиционирование вывода по YYYY/MM/DD, обход data-папок, манипуляции с суффиксами при конвертации форматов.

В следующем уроке — datetime и часовые пояса. Это та область, где чаще всего возникают баги в дата-инженерных pipeline’ах: на UTC vs локальное время теряют по нескольку часов в неделю.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6