pathlib в Python: почему пора забыть про склейку строк
Ручная склейка путей через os.path.join и конкатенацию ломается на Windows, теряет расширения и молчит об ошибках. Разбираем, как pathlib решает это и где сам ведёт себя неочевидно.

Строка path = folder + "/" + filename работает ровно до первого запуска на Windows или до первого имени файла с лишним слешем на конце. Дальше начинается тихая порча данных: дублированные разделители, пути вида data//report.csv, потерянные расширения при наивной обрезке. Модуль pathlib из стандартной библиотеки (стабилен с Python 3.4, активно дорабатывается вплоть до 3.12) закрывает эти классы ошибок за счёт того, что путь у него — объект, а не строка. Разберём, где привычная склейка даёт сбой, что pathlib делает иначе и в каких местах он сам ведёт себя не так, как ожидаешь.
Где ломается склейка строк
Проблема не в том, что конкатенация «некрасивая», а в том, что она молча выдаёт неверный результат в предсказуемых ситуациях.
- Разделитель зашит в код.
base + "/" + nameна Windows формально сработает — WinAPI понимает прямой слеш, — но сравнение путей, логи и любые операции со строкой пути начнут расходиться с тем, что показывает проводник (C:\data\report.csvпротивC:\data/report.csv). - Двойные разделители. Если
baseуже заканчивается на/, получитеdata//report.csv. Большинство ОС это переварит, но проверка «два пути равны» строковым сравнением упадёт. - Ручной разбор расширения.
name.split(".")[0]дляarchive.tar.gzвернётarchive, а неarchive.tar. А для файла.gitignoreотдаст пустую строку. - os.path.join с абсолютным аргументом.
os.path.join("/etc", "/passwd")вернёт/passwd— второй абсолютный путь затирает первый. Это документированное поведение, но на нём регулярно спотыкаются.
Как pathlib решает те же задачи
Центральный класс — Path. При создании он подставляет правильный класс под текущую ОС (PosixPath или WindowsPath), а оператор / перегружен для соединения частей пути.
from pathlib import Path
# Соединение через / — разделитель подставит сама ОС
base = Path("data")
report = base / "2024" / "report.csv"
print(report) # data/2024/report.csv на Linux, data\2024\report.csv на Windows
# Разбор имени и расширений — без split по точке
print(report.name) # report.csv
print(report.stem) # report
print(report.suffix) # .csv
archive = Path("backup.tar.gz")
print(archive.suffixes) # ['.tar', '.gz']
print(archive.suffix) # .gz
# Двойные слеши схлопываются при нормализации представления
messy = Path("data/") / "report.csv"
print(messy) # data/report.csv
Оператор / корректно обрабатывает и абсолютные части: Path("/etc") / "/passwd" вернёт /passwd — здесь поведение совпадает с os.path.join, так что об этом всё равно нужно помнить.
Чтение и запись файлов без open
Для мелких операций Path даёт короткие методы, которые сами открывают и закрывают файл.
from pathlib import Path
config = Path("settings.txt")
# Записать текст (перезапишет файл целиком)
config.write_text("debug=true\n", encoding="utf-8")
# Прочитать целиком
content = config.read_text(encoding="utf-8")
# Для бинарных данных — write_bytes / read_bytes
Всегда указывайте encoding явно: без него берётся системная кодировка, и один и тот же код на Linux и Windows прочитает файл по-разному. Для больших файлов эти методы не подходят — они читают всё в память, тут по-прежнему нужен with config.open(...).
Обход директорий
from pathlib import Path
src = Path("src")
# Все .py в директории и подпапках
for py_file in src.rglob("*.py"):
print(py_file, py_file.stat().st_size)
# Только на верхнем уровне — glob вместо rglob
for item in src.glob("*.py"):
print(item)
Что выбрать: строки, os.path или pathlib
| Задача | Склейка строк | os.path | pathlib |
|---|---|---|---|
| Соединить части пути | Ошибки с разделителями | os.path.join | /, читается лучше |
| Расширение файла | Ручной split, багует | os.path.splitext | .suffix, .suffixes |
| Проверить существование | — | os.path.exists | .exists() |
| Обход дерева | — | os.walk | .rglob() |
| Работа с sys.path, зависимости на строки | — | строки «из коробки» | нужен str(path) |
Практический критерий: новый код пишите на pathlib. Переходите на os.path или явно приводите путь к строке через str(), когда сторонняя библиотека принимает только строку и не знает про объекты Path.
Путь — это не строка, которая случайно содержит слеши. Это структура с именем, родителем, расширением и разделителем, зависящим от ОС. pathlib делает эту структуру явной, и половина «странных багов с путями» исчезает вместе со склейкой.
Где pathlib ведёт себя неочевидно
- Path не проверяет, что файл существует.
Path("нет_такого.txt")создаётся без ошибки. Проверять надо явно через.exists()или.is_file(), иначе исключение прилетит только при попытке чтения. - Не все библиотеки принимают Path. Большинство стандартных функций поддерживают протокол
os.PathLikeс Python 3.6, но встречаются пакеты, которые делают внутри операции над строкой и падают на объекте. Симптом —TypeErrorвроде expected str, bytes or os.PathLike или неожиданное поведение. Лечитсяstr(path). - resolve() ходит в файловую систему.
.resolve()раскрывает симлинки и относительные части, обращаясь к диску. В Python 3.6+ он не падает на несуществующем пути, но по умолчанию это медленнее, чем строковая нормализация. Если симлинки раскрывать не нужно — беритеos.path.normpath(str(path))или сравнивайте.parts. - Сравнение путей регистрозависимо. На Windows файловая система обычно нечувствительна к регистру, но
Path("File.txt") == Path("file.txt")вернётFalse. Для надёжного сравнения приводите обе стороны к одному виду.
Куда двигаться дальше
Начните с одного файла: замените в нём все os.path.join и конкатенации на /, а разбор расширений — на .suffix/.stem. Дальше стоит посмотреть в сторону трёх вещей.
- PurePath — если нужно манипулировать путями без обращения к диску (например, разбирать пути другой ОС в кроссплатформенном коде):
PureWindowsPathиPurePosixPath. - Path.cwd() и Path(__file__).parent — для построения путей относительно расположения скрипта, а не текущей рабочей директории. Это убирает класс багов «запустил из другой папки — всё сломалось».
- Официальная документация модуля — там перечислены методы, доступные именно в вашей версии Python. Часть возможностей добавлялась постепенно (например,
Path.walk()появился в 3.12), так что версию сверяйте.
Субтитры и таймкоды: Расшифровка речи и субтитры по правилам читаемости: 42 знака в строке, две строки, минимум секунда на реплику.
Спросить за 40 ₽Источники: pathlib — Object-oriented filesystem paths (Python docs), os.path — Common pathname manipulations (Python docs)
Частые вопросы
У меня Python 3.6, а в примерах упоминается 3.12. Всё это заработает?
Стоит ли переписывать весь старый код с os.path на pathlib?
Библиотека выдаёт TypeError на объекте Path — что делать?
pathlib медленнее склейки строк? Это важно?
Как безопасно строить путь относительно самого скрипта, а не рабочей директории?
Как правильно достать имя файла без расширения, если расширений несколько?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.