Почему именно 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(...).
Три разные стратегии: один уровень, шаблон, рекурсивно
Простое 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.path | pathlib |
|---|---|---|
| Склейка путей | 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 локальное время теряют по нескольку часов в неделю.