Работа с файлами в Python: open, with и кодировка

Python Автор: Среда и версия: CPython 3.14.5
содержание

Файл в Python открывают функцией open, а закрывают почти всегда автоматически, конструкцией with. Два аргумента решают всё: режим доступа и кодировка. Ошибки с файлами тоже сводятся к этим двум. Код выполнен на CPython 3.14.5; блоки здесь не запускаются в браузере, потому что трогают файловую систему.

Python / 01

Режим открытия определяет судьбу старого текста

Режим открытия определяет судьбу старого текста01 w / перезапись старое → новое Файл очищается при открытии. Запись заменяет содержимое. 02 a / добавление старое + новое Запись идёт в конец. Прежнее содержимое сохраняется.01w / перезаписьстарое → новоеФайл очищается при открытии. Записьзаменяет содержимое.02a / добавлениестарое + новоеЗапись идёт в конец. Прежнее содержимоесохраняется.
Режим "w" очищает файл уже при открытии. Режим "a" сохраняет содержимое и пишет в конец.

Как открыть файл: with и open

with гарантирует закрытие файла при выходе из блока, в том числе при исключении:

with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("первая строка\n")
    f.write("вторая строка\n")

with open("notes.txt", encoding="utf-8") as f:
    print(f.read())
первая строка
вторая строка

Перевод строки write сам не добавляет: пишешь построчно — ставь \n руками. Режим "r" подразумевается, поэтому во втором вызове его нет.

Без with файл остаётся открытым, пока не вызовешь close():

f = open("notes.txt", encoding="utf-8")
print(f.closed)
f.close()
print(f.closed)
False
True

Разница не косметическая. Если между open и close произойдёт исключение, close не выполнится, и файл останется открытым до сборки мусора, а на запись это ещё и означает потерянные данные из буфера. with закрывает в любом случае:

try:
    with open("notes.txt", encoding="utf-8") as f:
        raise ValueError("сломалось внутри блока")
except ValueError:
    pass
print("закрыт после исключения:", f.closed)
закрыт после исключения: True

Как устроен этот протокол изнутри, разобрано в статье про контекстные менеджеры. Обращение к уже закрытому файлу даёт понятную ошибку:

f.read()
ValueError: I/O operation on closed file.

Как прочитать файл

Три способа с разным аппетитом к памяти. read() берёт весь файл одной строкой, перебор в цикле идёт построчно:

with open("notes.txt", encoding="utf-8") as f:
    for line in f:
        print(repr(line))
'первая строка\n'
'вторая строка\n'

repr здесь показывает главное: перевод строки остаётся в конце каждой строки. Его убирают rstrip("\n"), а не strip(), иначе заодно пропадут значимые пробелы по краям:

with open("notes.txt", encoding="utf-8") as f:
    lines = [line.rstrip("\n") for line in f]
print(lines)
['первая строка', 'вторая строка']

Перебор циклом читает файл порциями и не держит его целиком в памяти. На файле из ста тысяч строк это единственный разумный способ:

with open("big.txt", "w", encoding="utf-8") as f:
    for i in range(100000):
        f.write(f"строка {i}\n")

total = 0
with open("big.txt", encoding="utf-8") as f:
    for line in f:
        total += 1
print(total)
100000

Метод readlines() вернул бы список из ста тысяч строк сразу, и на файле в несколько гигабайт программа просто не запустилась бы.

Режимы: r, w, a и x

Второй аргумент open решает, что случится с существующим файлом.

режимчто делаетфайла нет
rчтение, по умолчаниюFileNotFoundError
wзапись, стирает содержимоесоздаёт
aдописывание в конецсоздаёт
xсоздание, отказ если файл естьсоздаёт

Режим w стирает файл в момент открытия, до первой записи:

with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("только эта строка\n")
with open("notes.txt", encoding="utf-8") as f:
    print(f.read().strip())
только эта строка

Двух прежних строк больше нет. Это самая дорогая опечатка в теме: перепутал "w" с "a" — и файл, который собирался дополнить, оказался пустым.

with open("notes.txt", "a", encoding="utf-8") as f:
    f.write("дописанная\n")
with open("notes.txt", encoding="utf-8") as f:
    print(f.read().strip())
только эта строка
дописанная

Режим x защищает от случайной перезаписи, отказываясь работать с существующим файлом:

open("notes.txt", "x", encoding="utf-8")
FileExistsError: [Errno 17] File exists: 'notes.txt'

А чтение несуществующего файла даёт свою ошибку, и её обычно перехватывают:

open("нет-такого.txt", encoding="utf-8")
FileNotFoundError: [Errno 2] No such file or directory: 'нет-такого.txt'

Как её ловить и почему try вокруг одной строки лучше, чем вокруг десяти, разобрано в статье про исключения.

Зачем всегда писать encoding

Без явного encoding Python берёт кодировку системы, а она разная на разных машинах. Файл, записанный на одной, читается кракозябрами на другой. Проверить умолчание можно так:

import locale

print(locale.getpreferredencoding(False))
UTF-8

На этой машине повезло, на Windows там исторически оказывается cp1251. Несовпадение кодировок может дать как ошибку, так и кракозябры без исключения. В этом примере ASCII-декодер отвергает байты UTF-8:

with open("cyr.txt", "w", encoding="utf-8") as f:
    f.write("Аня")

with open("cyr.txt", encoding="ascii") as f:
    f.read()
UnicodeDecodeError: 'ascii' codec can't decode byte 0xd0 in position 0: ordinal not in range(128)

Сообщение называет и кодек, и позицию проблемного байта. Правило простое: encoding="utf-8" пишут в каждом open, который работает с текстом. Для двоичных данных, картинок и архивов, берут режимы "rb" и "wb", и там кодировки нет вовсе.

pathlib: пути как объекты

Модуль pathlib заменяет склейку строк и половину вызовов open:

from pathlib import Path

p = Path("short.txt")
p.write_text("одной строкой\n", encoding="utf-8")
print(p.read_text(encoding="utf-8").strip())
print(p.exists(), p.stat().st_size, p.suffix, p.stem)
одной строкой
True 26 .txt short

Размер в байтах, а не в символах: тринадцать кириллических букв плюс перевод строки дали 26 байт, потому что в UTF-8 кириллица занимает два байта на символ.

Пути собирают оператором /, и он сам подставит правильный разделитель на любой системе:

from pathlib import Path

base = Path("data")
base.mkdir(exist_ok=True)
target = base / "report.csv"
target.write_text("id;name\n1;Аня\n", encoding="utf-8")
print(target, target.exists())
data/report.csv True

Аргумент exist_ok=True избавляет от проверки «а вдруг каталог уже есть». Содержимое каталога перебирают iterdir, а фильтруют по маске через glob:

from pathlib import Path

base = Path("data")
(base / "old.txt").write_text("архив", encoding="utf-8")
print(sorted(p.name for p in base.iterdir()))
print(sorted(p.name for p in base.glob("*.csv")))
['old.txt', 'report.csv']
['report.csv']

CSV и JSON: не разбирай руками

Соблазн прочитать CSV через split(",") велик и заканчивается одинаково. Запишем таблицу, где в одном поле есть запятая:

import csv

with open("people.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["name", "city"])
    writer.writerow(["Аня", "Москва"])
    writer.writerow(["Борис, младший", "Казань"])

print(open("people.csv", encoding="utf-8").read().rstrip())
name,city
Аня,Москва
"Борис, младший",Казань

Модуль сам обернул проблемное поле кавычками. Его же читалка разбирает файл верно:

import csv

with open("people.csv", encoding="utf-8", newline="") as f:
    for row in csv.reader(f):
        print(row)
['name', 'city']
['Аня', 'Москва']
['Борис, младший', 'Казань']

А наивное разбиение по запятой ломается ровно на этой строке:

with open("people.csv", encoding="utf-8") as f:
    next(f)
    for line in f:
        print(line.rstrip("\n").split(","))
['Аня', 'Москва']
['"Борис', ' младший"', 'Казань']

Вместо двух полей получилось три, да ещё с прилипшими кавычками. Первая строка при этом разобралась правильно, поэтому на маленькой выгрузке ошибку не замечают. Аргумент newline="" в open для csv обязателен: без него на Windows в файле появятся лишние пустые строки.

Для JSON работает тот же принцип, только модуль называется json:

import json
from pathlib import Path

data = {"name": "Аня", "orders": [1, 2]}
Path("data.json").write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
print(Path("data.json").read_text(encoding="utf-8"))
{"name": "Аня", "orders": [1, 2]}

Аргумент ensure_ascii=False оставляет кириллицу читаемой; без него в файл уедет \u0410\u043d\u044f — валидный JSON, который прочитается обратно правильно, но глазами не читается. Разобранный JSON превращается в обычный словарь. Всё остальное про формат — dumps против dump, что теряется на обороте, datetime и Decimal, разбор JSONDecodeError — в отдельной статье про JSON.

Ошибки, на которых спотыкаются

"w" вместо "a". Файл стирается в момент открытия, ещё до первой записи. Данных не вернуть.

Забытый encoding. Работает на твоей машине, ломается на чужой. Пиши явно всегда.

strip() вместо rstrip("\n"). Убирает не только перевод строки, но и значащие пробелы по краям поля.

Чтение через readlines() по привычке. На большом файле съедает память целиком, хотя обычный цикл по файлу решает ту же задачу порциями.

Разбор CSV через split. Ломается на первой же запятой внутри поля, и обычно это происходит на проде, а не на тестовой выгрузке.

Относительный путь. open("data.txt") ищет файл относительно текущего рабочего каталога, а не относительно файла со скриптом. Если запуск идёт из другого места, файл «пропадает». Надёжный путь строят от Path(__file__).parent.

Частые вопросы

Как прочитать файл в список строк

[line.rstrip("\n") for line in f] внутри with. Метод readlines() делает почти то же самое, но оставляет переводы строк и читает файл целиком.

Как дописать в файл, а не перезаписать

Режимом "a". Режим "w" стирает содержимое при открытии.

Как проверить, существует ли файл

Path("файл.txt").exists(). Но если сразу после проверки ты его открываешь, проверка лишняя: между ней и открытием файл может исчезнуть, поэтому надёжнее просто открыть и перехватить FileNotFoundError.

Чем pathlib лучше os.path

Пути становятся объектами с методами, а склейка через / читается лучше вложенных os.path.join. Функциональность пересекается, pathlib короче.

Как работать с Excel

Встроенными средствами никак: .xlsx это архив с XML внутри. Берут стороннюю библиотеку, чаще openpyxl или pandas. Если данные можно выгрузить в CSV, это дешевле и не тянет зависимостей.

Где потренироваться

В пути «Python с нуля» на Koddo файлы идут после строк и списков: сначала чтение построчно, потом разбор строки на поля, потом запись отчёта. Проверить себя можно на дедупликации событий — типичная задача над содержимым выгрузки.

Источники