Python: работа с файлами

Курс посвящён чтению, записи и управлению файлами в Python на практике. Рассматриваются текстовые и бинарные форматы, обработка путей и каталогов, а также типичные ошибки и безопасные приёмы работы с данными.

1. Основы ввода-вывода: open(), режимы и кодировки

Основы ввода-вывода: open(), режимы и кодировки

Работа с файлами в Python начинается с правильного открытия файла: нужно выбрать режим (что мы хотим делать с файлом) и понять, в каком виде данные будут читаться/записываться (текст или байты), а также как связана текстовая работа с кодировкой.

В этой статье вы научитесь:

  • Открывать файлы через open() и получать файловый объект
  • Выбирать режимы открытия (r, w, a, x, b, t, +)
  • Понимать разницу между текстовым и бинарным режимами
  • Работать с кодировками и избегать типичных ошибок
  • Правильно закрывать файлы с помощью with
  • Что такое ввод-вывод и что возвращает open()

    Файлы — это внешний источник данных. Python общается с ними через потоки ввода-вывода: вы читаете данные из файла в программу и записываете из программы в файл.

    Функция open() открывает файл и возвращает файловый объект (объект-поток), у которого есть методы чтения и записи.

    Простейший пример:

    Здесь:

  • open(...) — открывает файл и создаёт файловый объект
  • read() — читает содержимое
  • close() — закрывает файл и освобождает ресурс
  • Важно: файл нужно закрывать всегда, иначе можно получить утечки ресурсов, незаписанные данные (из-за буферизации) или блокировки файла на некоторых ОС.

    !Схема показывает два правильных сценария: ручное закрытие через close() и безопасный вариант через with.

    Почему лучше использовать with

    Конструкция with использует контекстный менеджер: файл закроется автоматически даже если внутри блока возникнет ошибка.

    Это базовая практика, которую стоит применять почти всегда.

    Сигнатура open() и ключевые параметры

    На практике чаще всего достаточно file, mode и encoding (для текста), но полезно знать основные параметры.

    Пример с явными именованными аргументами:

    Ключевые параметры:

  • file — путь к файлу (строка или объект пути)
  • mode — режим открытия (чтение/запись/добавление, текст/байты и т.д.)
  • encoding — кодировка только для текстового режима
  • errors — стратегия обработки ошибок декодирования/кодирования ("strict", "replace", "ignore" и другие)
  • newline — управление переводами строк в текстовом режиме
  • Официальная документация:

  • open() — встроенная функция
  • Модуль io — потоки ввода-вывода
  • Режимы открытия: как выбрать правильный

    Режим — это строка, составленная из символов. Важнее всего понять базовую операцию (r, w, a, x) и формат данных (t или b).

    Базовые режимы

    | Режим | Смысл | Если файла нет | Если файл есть | |---|---|---|---| | r | чтение | ошибка | читаем | | w | запись | создаст | перезапишет (обнулит) | | a | добавление в конец | создаст | допишет в конец | | x | создать новый и писать | создаст | ошибка |

    Примеры:

    Текстовый и бинарный режимы

    | Суффикс | Режим | Что читаем/пишем | Тип данных | |---|---|---|---| | t | текстовый (по умолчанию) | символы | str | | b | бинарный | байты | bytes |

    Примеры:

    В бинарном режиме параметр encoding не используется (и не нужен), потому что Python не декодирует байты в текст.

    Режимы с плюсом: чтение и запись

    Символ + означает, что файл открыт и на чтение, и на запись.

    Типичные варианты:

  • r+ — чтение и запись, файл должен существовать
  • w+ — чтение и запись, файл будет создан или перезаписан
  • a+ — чтение и запись, запись всегда в конец
  • Мини-пример (обратите внимание на позицию указателя):

    Метод seek(0) перемещает позицию чтения/записи в начало файла. Позиция важна в режимах с +.

    Пути к файлам: где Python ищет файл

    Если вы передаёте относительный путь, он считается относительно текущей рабочей директории (current working directory).

    Пример:

    Если файл «не находится», проблема часто не в open(), а в том, что программа запускается из другой папки.

    Для более надёжной работы с путями часто используют pathlib:

    Кодировки: как текст превращается в байты и обратно

    Файл на диске хранит байты. Когда вы открываете файл в текстовом режиме (t), Python:

  • при чтении: декодирует байты в str с помощью encoding
  • при записи: кодирует str в байты с помощью encoding
  • Почему важно явно указывать encoding

    Если не указать encoding, Python выберет системную кодировку по умолчанию. Она может отличаться на разных машинах и ОС. Результат — «кракозябры» или ошибка декодирования.

    Практика для учебных и большинства прикладных задач: открывать текстовые файлы так:

    utf-8 — наиболее распространённая и переносимая кодировка.

    Типичная ошибка: UnicodeDecodeError

    Если файл сохранён в одной кодировке, а вы читаете в другой, можно получить ошибку вроде UnicodeDecodeError.

    Стратегии решения:

  • Узнать реальную кодировку файла и указать её в encoding
  • Если данные «грязные», временно использовать errors="replace" (символы заменятся на `), чтобы хотя бы прочитать остальное
  • Пример:

    Используйте errors="replace" осознанно: это не «исправление файла», а компромисс, чтобы не падать с ошибкой.

    Минимальные операции чтения и записи

    После открытия файла в правильном режиме вы используете методы файлового объекта.

    Чтение:

  • read() — прочитать всё
  • read(n) — прочитать n символов (или байт в бинарном режиме)
  • readline() — прочитать одну строку
  • итерация по файлу — читать построчно без загрузки всего файла в память
  • Пример построчного чтения:

    Запись:

  • write(str) — записать строку
  • writelines(iterable) — записать набор строк (без автоматических \n)
  • Пример:

    Частые ошибки и как их избежать

  • Перезаписали файл вместо добавления
  • - Используйте a, если нужно дописывать, и x, если нельзя перетирать существующий файл
  • Забыли указать кодировку
  • - В учебных и кросс-платформенных сценариях указывайте
    encoding="utf-8"
  • Читаете бинарный файл в текстовом режиме
  • - Для изображений, архивов, pdf и т.п. используйте
    rb/wb
  • Не закрыли файл
  • - Используйте
    with open(...) as f:

    Итоги

  • open() возвращает файловый объект, через который выполняются операции чтения/записи
  • Режимы определяют поведение: r/w/a/x, формат данных: t/b, совмещённый доступ: +
  • В текстовом режиме всегда думайте о кодировке и чаще всего указывайте encoding="utf-8"
  • Используйте with`, чтобы файл закрывался автоматически
  • В следующих материалах курса мы будем углубляться в практику чтения и записи: построчное чтение, работа с указателем файла, обработка больших файлов и типичные шаблоны обработки данных.

    2. Чтение и запись текста: контекстный менеджер и буферизация

    Чтение и запись текста: контекстный менеджер и буферизация

    В прошлой статье вы разобрали, как работает open(), чем отличаются режимы (r, w, a, b, t, +) и почему для текстовых файлов важно задавать encoding.

    Теперь сфокусируемся на практике работы с текстом:

  • как читать и писать текст безопасно через with
  • какие есть удобные шаблоны чтения (целиком, построчно, кусками)
  • что такое буферизация при чтении и записи и как она влияет на скорость и момент появления данных в файле
  • Полезные источники:

  • Документация open()
  • Документация по контекстным менеджерам (with)
  • Модуль io (потоки ввода-вывода)
  • Контекстный менеджер: что именно делает with

    Конструкция with open(...) as f: решает сразу несколько задач:

  • гарантирует закрытие файла
  • при закрытии автоматически выполняет сброс буфера записи (flush), чтобы данные точно попали на диск
  • освобождает системные ресурсы даже при исключениях
  • Пример: безопасно читаем файл целиком.

    Пример: безопасно пишем файл.

    Если внутри блока случится ошибка, файл всё равно будет закрыт.

    Чтение текста: три основных подхода

    Выбор подхода зависит от размера файла и того, как вы планируете обрабатывать данные.

    Чтение целиком: read()

    Подходит для небольших файлов.

    Минус: большой файл может занять много памяти.

    Построчное чтение: цикл по файловому объекту

    Это самый практичный шаблон для больших текстовых файлов: память расходуется экономно, строки приходят по мере чтения.

    Здесь rstrip("\n") убирает символ перевода строки в конце строки (часто это нужно, чтобы не получать лишние пустые строки при print).

    Чтение кусками: read(n)

    Нужно, когда вы обрабатываете поток постепенно (например, считаете статистику) и не хотите читать файл целиком. В текстовом режиме n означает количество символов.

    > В бинарном режиме ("rb") read(n) читает n байт, а в текстовом ("r") — n символов после декодирования.

    Запись текста: write(), writelines() и print(file=...)

    write(): пишем то, что контролируем

    write() записывает строку как есть и возвращает количество записанных символов.

    Важно: write() не добавляет \n автоматически.

    writelines(): быстро записываем набор строк

    writelines() принимает последовательность строк и записывает их подряд. Переводы строк нужно добавить самим.

    print(..., file=f): удобно для человеко-читаемого вывода

    print() сам добавляет перевод строки по умолчанию и умеет писать в открытый файл.

    print() особенно удобен для логов и отчётов, где важна читаемость.

    Буферизация: почему данные не всегда сразу оказываются в файле

    При записи в файл Python обычно не отправляет каждый символ на диск немедленно. Вместо этого данные накапливаются в буфере и сбрасываются порциями. Это резко ускоряет работу: меньше системных вызовов, меньше накладных расходов.

    !Как данные проходят путь от вызова write() до файла на диске и где появляется буфер

    Когда буфер обычно сбрасывается

    Буфер записи может быть сброшен в файл, когда:

  • буфер заполнился
  • вы вызвали f.flush() вручную
  • файл закрывается (в том числе автоматически при выходе из with)
  • Пример: принудительный сброс буфера.

    flush() полезен, когда:

  • вы пишете лог и хотите видеть его на диске во время работы программы
  • программа может аварийно завершиться, и вы хотите минимизировать потери последних записей
  • При этом flush() не делает запись “бесплатной”: частые flush могут замедлить программу.

    Управление буферизацией через open(..., buffering=...)

    У open() есть параметр buffering, который управляет буферизацией.

    Общее правило:

  • buffering=0 разрешён только в бинарном режиме (например, "wb"), в текстовом режиме так делать нельзя
  • buffering=1 означает построчную буферизацию (line buffering) и работает в текстовом режиме
  • buffering>1 задаёт примерный размер буфера в байтах (актуальнее для бинарных потоков; для текстовых потоков поведение зависит от реализации)
  • если buffering не указан, используется значение по умолчанию, которое обычно достаточно
  • Пример построчной буферизации: часто применяют для логов.

    Смысл: после каждой строки вероятность быстрого сброса выше, но точное поведение всё равно зависит от среды выполнения.

    Практические шаблоны

    Фильтрация строк и запись результата в новый файл

    Здесь удобно то, что оба файла закроются автоматически.

    Безопасная перезапись: пишем во временный файл и заменяем

    Это шаблон, который снижает риск оставить “битый” файл, если программа упадёт посередине записи.

  • Пишем результат в новый файл, например data.txt.tmp.
  • Закрываем его (выходим из with).
  • Заменяем старый файл новым.
  • Для замены чаще используют pathlib и replace().

    Типичные ошибки и правильные привычки

  • Забыли добавить \n при записи через write()
  • - write() не добавляет перевод строки. Если пишете строки, добавляйте \n сами или используйте print(file=...).
  • Читаете большой файл через read()
  • - Для больших файлов используйте построчное чтение: for line in f:.
  • Ожидаете увидеть данные в файле “сразу”, но они в буфере
  • - Используйте flush() или выходите из with (файл закроется и буфер будет сброшен).
  • Открываете текст без encoding
  • - Для переносимости чаще всего используйте encoding="utf-8".

    Итоги

  • with open(...) as f: — базовый способ работать с файлами: он гарантирует закрытие и помогает не терять данные из-за буферизации
  • читать текст можно целиком (read()), построчно (for line in f) или кусками (read(n)), и выбор зависит от размера файла
  • запись идёт через write(), writelines() или print(file=...), а попадание данных на диск может быть отложено из-за буферизации
  • flush() и параметр buffering позволяют управлять тем, когда данные “выплывают” из буфера
  • 3. Файловая система: пути, каталоги и модуль pathlib

    Файловая система: пути, каталоги и модуль pathlib

    В предыдущих статьях вы научились открывать файлы через open(), выбирать режимы, работать с кодировками и безопасно читать/писать текст через with с учётом буферизации.

    Следующий логичный шаг — научиться надёжно находить файлы и папки и управлять ими: правильно составлять пути, создавать каталоги, обходить дерево проекта и делать код переносимым между Windows, macOS и Linux. Для этого в современном Python чаще всего используют модуль pathlib.

    В этой статье вы научитесь:

  • Отличать абсолютные и относительные пути
  • Понимать роль текущей рабочей директории
  • Создавать пути и работать с ними через pathlib.Path
  • Проверять существование файлов и папок, создавать каталоги
  • Перебирать содержимое директорий и искать файлы через glob()
  • Использовать Path.open() вместе с with вместо ручной сборки строк путей
  • Полезные источники:

  • Документация pathlib
  • Документация os.path
  • Что такое путь и почему со строками легко ошибиться

    Путь — это адрес файла или каталога в файловой системе.

    Типичные проблемы, когда пути хранятся просто как строки:

  • Разные разделители: \ в Windows и / в Unix-системах
  • Сложнее безопасно “склеивать” части пути
  • Появляются ошибки из-за лишних или недостающих слэшей
  • Модуль pathlib решает это тем, что даёт объект Path, который умеет корректно собирать пути и выполнять операции с файлами и папками.

    Абсолютные и относительные пути

    Абсолютный путь

    Абсолютный путь начинается от “корня” файловой системы.

  • На Unix: обычно начинается с /, например /home/user/project/data.txt
  • На Windows: обычно начинается с буквы диска, например C:\Users\User\project\data.txt
  • Абсолютный путь однозначно указывает место файла.

    Относительный путь

    Относительный путь считается от текущей рабочей директории (её часто называют CWD, current working directory).

    Пример относительного пути:

  • data/input.txt
  • Если CWD — это папка проекта, то такой путь указывает на файл внутри data.

    > Частая причина ошибки “файл не найден” — программа запускается не из той папки, где вы ожидаете.

    Текущая рабочая директория и почему она важна

    В Python вы можете узнать текущую рабочую директорию так:

    Если вы используете относительные пути, они интерпретируются относительно Path.cwd().

    Практическая рекомендация для учебных проектов и небольших скриптов:

  • Храните данные в папке проекта (например, data/)
  • Работайте с путями через Path, а не через конкатенацию строк
  • pathlib.Path: базовые идеи

    pathlib.Path — это объект, который представляет путь.

    Создание объекта:

    Оператор / у Path означает “присоединить следующий фрагмент пути” и делает код переносимым.

    !Как Path собирает путь из частей и почему код переносим

    Полезные свойства Path

    Это удобнее и надёжнее, чем вручную “разбирать” строку.

    Открытие файлов через Path.open() и связь с прошлыми темами

    В прошлых статьях вы работали с open(). С pathlib вы обычно открываете файл так же, но через метод open() у объекта пути.

    Это особенно удобно, когда путь собирается из частей.

    Важно:

  • Режимы ("r", "w", "a", "b", "+") остаются теми же
  • Про encoding="utf-8" в текстовом режиме нужно помнить так же, как и раньше
  • with по-прежнему гарантирует закрытие файла и сброс буфера
  • Проверки: существует ли путь и что это такое

    Перед чтением или обработкой часто нужно проверить, что путь существует и что это именно файл или папка.

    Практический шаблон:

    Создание каталогов

    Чтобы создать папку, используют mkdir().

    Если нужно создать сразу цепочку папок (например, output/reports/2026/), используйте parents=True.

    Здесь:

  • parents=True создаёт недостающие родительские папки
  • exist_ok=True не считает ошибкой ситуацию, когда папка уже существует
  • Перебор файлов и папок в каталоге

    iterdir(): список непосредственных элементов

    iterdir() возвращает элементы только текущей папки, без рекурсивного обхода.

    glob(): поиск по шаблону в одной папке

    Пример: найти все .txt в data/.

    rglob(): рекурсивный поиск по шаблону

    Пример: найти все .log внутри logs/ и её подпапок.

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

    pathlib даёт несколько похожих методов, и важно понимать их смысл.

    | Операция | Метод | Идея | |---|---|---| | Переименовать или переместить | rename() | “перемести по новому пути” | | Заменить целевой файл | replace() | “замени, даже если цель существует” |

    Пример переименования:

    Пример безопасной перезаписи через временный файл (связь с прошлой статьёй):

    Здесь replace() помогает сделать “атомарную” замену: старый файл заменяется новым после успешной записи.

    Домашняя папка и относительность к пользователю

    Иногда нужно строить путь относительно домашней директории пользователя.

    Это переносимее, чем жёстко прописывать C:\Users\... или /home/....

    os.path и pathlib: что использовать

    В Python есть два популярных подхода:

  • os.path работает в основном со строками и функциями
  • pathlib работает с объектом Path и методами
  • Для нового кода чаще выбирают pathlib, потому что:

  • Путь можно собирать через / без ручных разделителей
  • Код лучше читается
  • Многие операции (проверки, поиск, создание папок) выражаются короче
  • При этом важно знать, что os.path никуда не исчез: вы будете встречать его в старом коде и библиотеках.

    Итоги

  • Относительные пути зависят от текущей рабочей директории, поэтому ошибки “не найден файл” часто связаны с CWD
  • pathlib.Path — удобный и переносимый способ строить пути и работать с файловой системой
  • Path.open() используется вместе с with так же, как open(), и сохраняет все правила про режимы и кодировки
  • Для обхода каталогов и поиска файлов используйте iterdir(), glob() и rglob()
  • Для создания папок используйте mkdir(parents=True, exist_ok=True)
  • Дальше, когда вы уверенно ориентируетесь в путях и каталогах, становится проще писать полноценные утилиты обработки данных: читать пачки файлов, писать отчёты в структуру папок и делать это переносимо между операционными системами.

    4. Бинарные файлы и форматы: bytes, pickle, JSON, CSV

    Бинарные файлы и форматы: bytes, pickle, JSON, CSV

    В прошлых статьях вы научились уверенно открывать файлы через open()/Path.open(), выбирать режимы, учитывать кодировки и читать/писать текст безопасно через with. Теперь расширим картину: данные в файлах бывают не только текстом.

    В этой статье вы разберёте:

  • чем текст отличается от байтов и зачем нужен бинарный режим rb/wb
  • как устроен тип bytes и чем он отличается от str
  • что такое сериализация и чем отличаются pickle, JSON и CSV
  • какие типичные ошибки возникают при работе с форматами и как их избежать
  • !Иллюстрация показывает, где появляется кодировка и какие форматы обычно текстовые или бинарные.

    Текст и байты: что реально лежит в файле

    Файл на диске всегда хранит байты. Разница между режимами открытия:

  • в текстовом режиме ("r", "w") Python автоматически преобразует байты в str и обратно, используя encoding
  • в бинарном режиме ("rb", "wb") Python ничего не декодирует и не кодирует: вы читаете и пишете bytes
  • Пример: чтение одного и того же файла по-разному.

    Ключевая идея:

  • str это текст (символы)
  • bytes это сырые байты (числа 0..255)
  • Официальные источники:

  • Документация open()
  • Документация bytes
  • Тип bytes: базовые операции, которые реально нужны

    Как получить bytes из str и обратно

    Чтобы превратить текст в байты, используют encode(). Чтобы превратить байты в текст, используют decode().

    Важно:

  • encode() и decode() всегда должны использовать согласованную кодировку
  • в бинарном режиме encoding не указывают, потому что преобразований нет
  • Индексация и “кусочки” bytes

    bytes можно индексировать и резать как последовательность, но элементы будут числами.

    Это полезно для чтения заголовков бинарных форматов, магических байтов и протоколов.

    Когда нужен бинарный режим

    Бинарный режим нужен, когда файл по смыслу не является текстом или вы не хотите, чтобы Python вмешивался:

  • изображения, архивы, pdf
  • произвольные бинарные форматы
  • сериализация в pickle
  • точный контроль над байтами (без преобразований переводов строк и кодировок)
  • Сериализация: зачем вообще нужны JSON, CSV и pickle

    Сериализация это преобразование объекта Python в формат, который можно сохранить в файл или передать по сети, а потом восстановить.

    Форматы решают разные задачи. Удобно думать так:

  • JSON нужен для обмена структурированными данными (конфиги, API), человеко-читаем, переносим
  • CSV нужен для табличных данных, совместим с Excel и BI-инструментами
  • pickle нужен для сохранения питоновских объектов “как есть”, но это формат только для Python и с важными ограничениями безопасности
  • JSON: текстовый формат для словарей и списков

    JSON хранится как текст, поэтому обычно используется текстовый режим файлов и encoding="utf-8".

    Документация:

  • Модуль json
  • Запись JSON в файл

    Параметры, которые чаще всего нужны:

  • ensure_ascii=False чтобы кириллица сохранялась читабельно, а не в виде \u042f...
  • indent=2 чтобы файл был удобен для просмотра и диффов
  • Чтение JSON из файла

    Ограничения JSON

    JSON поддерживает ограниченный набор типов:

  • объекты (в Python это dict со строковыми ключами)
  • массивы (в Python это list)
  • строки, числа, true/false, null
  • Если вы попытаетесь записать, например, datetime или Path, получите ошибку, пока не сделаете преобразование вручную.

    CSV: текстовый формат для таблиц

    CSV это тоже текстовый формат, но со своими ловушками: разделители, кавычки, переводы строк.

    Документация:

  • Модуль csv
  • Главная практика: открывать CSV с newline=""

    При работе с csv в текстовом режиме рекомендуется открывать файл с newline="". Это снижает риск получить “пустые строки” и некорректную обработку переводов строк, особенно на Windows.

    Чтение:

    Разделитель и региональные настройки

    В некоторых средах (например, в русской локали Excel) ожидается разделитель ; вместо ,. Тогда нужно явно указать delimiter.

    pickle: бинарный формат “для Python”, но с ограничениями

    pickle сохраняет и восстанавливает питоновские объекты. Это удобно для быстрых прототипов, кешей и внутренних задач, где вы контролируете источник файла.

    Документация:

  • Модуль pickle
  • Запись и чтение pickle

    pickle работает с байтовыми потоками, поэтому открываем файл в бинарном режиме.

    Почему pickle может быть опасен

    pickle.load() может выполнить произвольный код при загрузке, если файл был подменён или пришёл из недоверенного источника.

    > Никогда не используйте pickle.load() для данных, полученных из недоверенных источников. Документация pickle — предупреждение о безопасности

    Практическое правило:

  • pickle используйте только для “внутренних” файлов, которые создаёт и читает ваше приложение, и которые нельзя подменить
  • для обмена данными с внешним миром чаще используйте JSON или другие безопасные форматы
  • Сравнение форматов: что выбрать

    | Критерий | JSON | CSV | pickle | |---|---|---|---| | Тип файла | текст | текст | бинарный | | Читаемость человеком | высокая | средняя | низкая | | Тип данных | вложенные структуры | таблицы | почти любые объекты Python | | Переносимость между языками | высокая | высокая | низкая | | Безопасность при чтении из внешних источников | обычно безопасен | обычно безопасен | опасен при недоверенных данных | | Типичный режим open() | "r"/"w" + encoding | "r"/"w" + encoding + newline="" | "rb"/"wb" |

    Типичные ошибки и как их избегать

  • Путают str и bytes
  • - Если вы открыли файл как "rb", вы получите bytes, и строковые операции (.split(" ")) не подойдут без декодирования.
  • Открывают CSV без newline=""
  • - Используйте newline="" при чтении и записи через csv.
  • Пишут JSON без ensure_ascii=False
  • - Файл будет корректным, но кириллица станет менее читабельной.
  • Используют pickle “для удобства” в задачах обмена данными
  • - Для внешних данных выбирайте JSON/CSV, а pickle оставляйте для контролируемых кешей и внутренних артефактов.

    Итоги

  • В файлах всегда лежат байты, а encoding имеет смысл только в текстовом режиме
  • bytes это последовательность байтов, str это последовательность символов
  • JSON и CSV обычно читают и пишут в текстовом режиме с encoding="utf-8", а CSV часто требует newline=""
  • pickle читает и пишет в бинарном режиме (rb/wb), удобен для Python-объектов, но небезопасен для недоверенных источников
  • 5. Ошибки, исключения и безопасные шаблоны работы с файлами

    Ошибки, исключения и безопасные шаблоны работы с файлами

    В предыдущих статьях курса вы научились открывать файлы через open() и Path.open(), выбирать режимы и кодировки, читать и писать текст безопасно через with, работать с путями через pathlib, а также сохранять данные в JSON, CSV и pickle.

    Теперь добавим недостающий слой, без которого работа с файлами в реальных проектах почти всегда ломается: ошибки, исключения и безопасные шаблоны, которые помогают:

  • корректно реагировать на отсутствие файла, прав доступа и другие проблемы ОС
  • отличать ожидаемые ошибки от критических
  • не терять данные при записи (атомарная замена, временные файлы)
  • получать полезную диагностику вместо “что-то не работает”
  • Почему ошибки при работе с файлами неизбежны

    Файловый ввод-вывод зависит не только от вашего кода, но и от внешней среды. Ошибки могут возникать в любой точке:

  • путь неверный или изменился
  • файл удалили между проверкой и чтением
  • нет прав на чтение или запись
  • диск заполнен или файловая система стала недоступна
  • файл оказался в неожиданной кодировке
  • формат данных битый (например, некорректный JSON)
  • Поэтому хорошая практика: считать ошибки нормальным сценарием и проектировать код так, чтобы он либо восстанавливался, либо падал понятно и управляемо.

    Какие исключения встречаются чаще всего

    Python поднимает исключения, когда операция с файлом не может быть выполнена. Важно не пытаться ловить “всё подряд”, а понимать типичные классы проблем.

    | Исключение | Когда возникает | Типичный пример | |---|---|---| | FileNotFoundError | файла или папки нет | открыли Path("data.txt"), а файла нет | | PermissionError | нет прав доступа | попытка записать в системную папку | | FileExistsError | файл уже существует, а вы открыли с x | open("a.txt", "x") | | IsADirectoryError | ожидаете файл, а это папка | open("data", "r"), где data/ каталог | | NotADirectoryError | ожидаете папку, а это файл | обход Path("data"), но data файл | | UnicodeDecodeError | неверная кодировка при чтении текста | читаем не-utf-8 как utf-8 | | UnicodeEncodeError | символы не кодируются выбранной кодировкой при записи | пишем кириллицу в encoding="ascii" | | OSError | широкий класс ошибок ОС | диск заполнен, ошибка устройства и т.д. | | json.JSONDecodeError | JSON повреждён или не JSON | файл обрезан или содержит мусор | | csv.Error | ошибка парсинга/формата CSV | некорректные кавычки, диалект | | pickle.UnpicklingError | ошибка восстановления pickle | файл не pickle или повреждён |

    Документация:

  • Встроенные исключения Python
  • Модуль json
  • Модуль csv
  • Модуль pickle
  • Базовый шаблон обработки ошибок: try/except/else

    with решает проблему закрытия файла, но не решает проблему обработки ошибок. Обычно вы комбинируете with и try/except так:

  • with отвечает за корректное освобождение ресурса
  • try/except отвечает за корректную реакцию на сбой
  • !Схема показывает, где возникает исключение и что делает обработчик

    Пример: читаем обязательный файл конфигурации и даём понятную ошибку.

    Практические правила:

  • ловите конкретные исключения, которые вы ожидаете
  • формируйте сообщение, привязанное к контексту (какой путь, какая операция)
  • если ошибка критическая, лучше “упасть”, но понятно, чем тихо продолжить с битым состоянием
  • > “Errors should never pass silently. Unless explicitly silenced.” PEP 20 — The Zen of Python

    Почему не стоит делать except Exception

    Шаблон вида except Exception: ... часто скрывает настоящие проблемы:

  • вы можете “проглотить” баг в логике (например, TypeError)
  • диагностика станет хуже
  • программа продолжит работу в неверном состоянии
  • Если вам всё же нужно перехватить широкий класс ошибок, делайте это осознанно и как минимум сохраняйте диагностику.

    Документация:

  • Модуль traceback
  • Безопасные шаблоны чтения

    Шаблон для обязательного файла

    Если файл обязателен, лучше быстро проверить, что это именно файл, и падать с понятной ошибкой.

    Почему это полезно:

  • сообщение об ошибке становится яснее
  • вы явно фиксируете ожидание файл, а не “что-то по этому пути”
  • Шаблон для опционального файла

    Если файл может отсутствовать, это не всегда ошибка.

    Важно: отсутствие файла должно быть осознанно допустимым сценарием.

    Кодировки и UnicodeDecodeError: как реагировать правильно

    Если вы читаете текст как utf-8, а файл в другой кодировке, получите UnicodeDecodeError. Правильные стратегии:

  • узнать реальную кодировку и читать с ней
  • если данные “грязные”, временно использовать errors="replace", понимая, что часть символов будет заменена
  • Этот пример показывает идею: не скрывать проблему, а сделать её понятнее.

    Безопасные шаблоны записи

    Почему запись “в лоб” может быть опасной

    Если вы делаете так:

    и программа падает посередине, вы можете получить:

  • пустой файл (старое содержимое уже стерлось)
  • частично записанный файл
  • Атомарная замена через временный файл

    Шаблон:

  • пишем результат во временный файл рядом с целевым
  • закрываем временный файл
  • заменяем целевой через Path.replace()
  • Что даёт этот шаблон:

  • если программа упала во время записи, целевой файл не тронут
  • замена происходит после успешной записи
  • Документация:

  • Модуль tempfile
  • pathlib.Path.replace
  • Создание файла без риска перезаписи

    Если вы хотите гарантировать, что существующий файл не будет затёрт, используйте режим x.

    Если файл уже существует, будет FileExistsError. Это часто полезно для артефактов сборки и “одноразовых” выгрузок.

    Ошибки форматов: JSON, CSV, pickle

    JSON: корректно ловим битый файл

    json.load() поднимает json.JSONDecodeError, если файл не является корректным JSON.

    CSV: базовая защита и правильное открытие

    При работе через csv открывайте файл с newline="". Если CSV повреждён, возможен csv.Error.

    pickle: ошибки и безопасность

    pickle удобен, но:

  • при повреждении файла возможны pickle.UnpicklingError, EOFError и другие исключения
  • pickle.load() небезопасен для недоверенных источников
  • Если файл может быть подменён, используйте JSON/CSV или другие безопасные форматы.

    Документация:

  • Предупреждение о безопасности pickle
  • Как сохранять полезную диагностику

    Не теряйте исходную причину: raise ... from e

    Когда вы поднимаете более “прикладное” исключение, полезно сохранить первопричину.

    Так в traceback будет видно и ваше сообщение, и исходная ошибка.

    Документация:

  • PEP 409 и цепочки исключений
  • Логи вместо print

    Для прикладных программ лучше использовать logging, чтобы:

  • писать ошибки в файл
  • контролировать уровни (INFO, WARNING, ERROR)
  • сохранять traceback при необходимости
  • Документация:

  • Модуль logging
  • Итоги

  • with гарантирует закрытие файла, но обработку ошибок нужно проектировать отдельно
  • ловите конкретные исключения (FileNotFoundError, PermissionError, UnicodeDecodeError), а не “всё подряд”
  • для записи важных файлов используйте шаблон “временный файл → replace()”, чтобы не оставлять битые результаты
  • для JSON/CSV обрабатывайте ошибки формата (json.JSONDecodeError, csv.Error) и формируйте понятные сообщения
  • улучшайте диагностику через raise ... from e, traceback и logging