Работа с файлами в Python строится вокруг встроенной функции open(): она открывает файл по пути и возвращает файловый объект, через который программа читает и пишет данные, а затем файл закрывается — надежнее всего конструкцией with. Ниже — полный рабочий пример, таблица режимов, кодировки, чтение построчно и по частям, запись, типовые ошибки и короткий блок про pathlib. Все примеры проверены на Python 3.14.
Содержание
- Минимальный пример: записать и прочитать файл
- Текстовый и бинарный режим
- Режимы open(): таблица
- Кодировка: почему encoding лучше указывать всегда
- Чтение: read, readline, построчный цикл
- Запись: write, writelines, print(file=)
- Зачем with и что делает close()
- Ошибки при открытии и как их обрабатывать
- pathlib: современная работа с путями
- Надежная и безопасная запись
- Если не получилось
- Выводы
- Где применяется / связь с практикой
- FAQ
Четыре термина, которые важно не путать:
- путь — строка или объект
Path, указывающий, где лежит файл ("notes.txt","data/report.csv"); - файловый объект — то, что возвращает
open(); у него есть методыread(),write(),close(); - режим — что разрешено делать с файлом: читать, перезаписывать, дописывать;
- кодировка — правило перевода символов в байты и обратно (UTF-8, cp1251). Нужна только в текстовом режиме.
Минимальный пример: записать и прочитать файл
# Запись и чтение текстового файла
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("Первая строка\n")
f.write("Вторая строка\n")
with open("notes.txt", "r", encoding="utf-8") as f:
for number, line in enumerate(f, start=1):
print(number, line.rstrip("\n"))
print(f.closed)
Результат запуска:
1 Первая строка
2 Вторая строка
True
Что здесь происходит. Первый with открывает notes.txt в режиме "w": если файла нет, он создается, если есть — очищается. Второй with открывает тот же файл на чтение, и цикл for идет по нему построчно. Каждая строка приходит вместе с символом перевода строки \n, поэтому его срезают rstrip("\n"). Последняя строка показывает, что после выхода из блока with файл уже закрыт.
Текстовый и бинарный режим
Файл на диске — всегда последовательность байтов. Разница между режимами в том, кто их интерпретирует:
- текстовый режим (по умолчанию, буква
t): Python декодирует байты в строкиstrпо кодировке и приводит переводы строк к\n; - бинарный режим (буква
b): вы получаете и отдаетеbytesкак есть. Так работают с картинками, архивами, PDF.
Цепочка на одном символе: буква «П» — это строка str в программе, в UTF-8 она записывается двумя байтами D0 9F, а в cp1251 — одним байтом CF. Один и тот же текст в разных кодировках дает разные байты на диске.
Режимы open(): таблица
| Режим | Если файла нет | Если файл есть | Позиция | Чтение / запись |
|---|---|---|---|---|
r (по умолчанию) |
FileNotFoundError |
открывает | начало | только чтение |
w |
создает | очищает | начало | только запись |
a |
создает | сохраняет содержимое | конец | запись в конец |
x |
создает | FileExistsError |
начало | только запись |
r+ |
FileNotFoundError |
открывает без очистки | начало | чтение и запись |
w+ |
создает | очищает | начало | чтение и запись |
К любому режиму добавляется b для бинарной работы: rb, wb, ab. Частая ошибка — открыть рабочий файл в "w", чтобы что-то дописать: содержимое пропадет сразу при открытии, еще до первого write(). Для дописывания нужен "a", для защиты от случайной перезаписи — "x":
with open("notes.txt", "x", encoding="utf-8") as f:
f.write("перезапишу?")
FileExistsError: [Errno 17] File exists: 'notes.txt'
Кодировка: почему encoding лучше указывать всегда
Если encoding не передан, Python берет кодировку по умолчанию из настроек системы. В Linux и macOS это обычно UTF-8, в Windows с русской локалью — часто cp1251. Один и тот же скрипт без encoding может читать файл на одной машине и падать на другой.
Неверно — файл записан в cp1251, а читается как UTF-8:
with open("cp.txt", "w", encoding="cp1251") as f:
f.write("Привет")
with open("cp.txt", encoding="utf-8") as f:
print(f.read())
Результат:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xcf in position 0: invalid continuation byte
Байт 0xcf — это та самая «П» в cp1251. Исправление — открыть файл в той кодировке, в которой он записан. А если кодировка неизвестна, сначала посмотреть на сырые байты в режиме rb:
with open("cp.txt", encoding="cp1251") as f:
print(f.read())
with open("cp.txt", "rb") as f:
print(f.read())
Привет
b'\xcf\xf0\xe8\xe2\xe5\xf2'
Для новых файлов разумный выбор — encoding="utf-8" явно, в каждом open() в текстовом режиме.
Чтение: read, readline, построчный цикл
| Способ | Что возвращает | Когда брать |
|---|---|---|
f.read() |
весь файл одной строкой | файл заведомо небольшой |
f.read(n) |
до n символов (в b — байтов) |
чтение порциями |
f.readline() |
одну строку с \n |
нужна только первая строка или заголовок |
for line in f |
строки по одной | основной способ для текстовых файлов |
f.readlines() |
список всех строк | нужен список целиком, файл небольшой |
Поведение методов на файле из первого примера:
with open("notes.txt", encoding="utf-8") as f:
print(repr(f.read(5)))
print(repr(f.readline()))
print(repr(f.readline()))
print(repr(f.readline()))
print(repr(f.read()))
f.seek(0)
print(f.readlines())
'Перва'
'я строка\n'
'Вторая строка\n'
''
''
['Первая строка\n', 'Вторая строка\n']
Из вывода видно три правила. read(5) в текстовом режиме считает символы, а не байты. readline() продолжает с текущей позиции, а в конце файла возвращает пустую строку '' — это и есть признак конца, исключения нет. Чтобы прочитать файл еще раз, позицию возвращают в начало через f.seek(0).
Цикл for line in f не загружает весь файл в память, поэтому подходит и для логов на гигабайты. Для бинарных файлов большого размера читают кусками фиксированной длины:
def read_chunks(path, size=64 * 1024):
with open(path, "rb") as f:
while chunk := f.read(size):
yield chunk
total = sum(len(c) for c in read_chunks("notes.txt", size=8))
print(total)
Результат — 52: столько байтов в двух строках первого примера (кириллица в UTF-8 занимает по 2 байта на букву).
Запись: write, writelines, print(file=)
with open("log.txt", "w", encoding="utf-8") as f:
n = f.write("старт\n")
print(n)
f.writelines(["a", "b"])
print("конец", file=f)
with open("log.txt", "a", encoding="utf-8") as f:
f.write("дописано\n")
with open("log.txt", encoding="utf-8") as f:
print(f.read(), end="")
6
старт
abконец
дописано
write() возвращает число записанных символов (6: пять букв и \n) и перевод строки сам не добавляет. writelines() тоже не добавляет разделители — поэтому a и b склеились с «конец». print(..., file=f) ставит \n сам. Режим "a" дописал строку в конец, не тронув старое содержимое.
Зачем with и что делает close()
Открытый файл занимает системный ресурс (дескриптор), а записанные данные могут лежать в буфере и еще не попасть на диск. close() сбрасывает буфер и освобождает дескриптор. Конструкция with — менеджер контекста — вызывает close() автоматически при выходе из блока, в том числе если внутри блока возникло исключение.
Без with легко забыть close() или не дойти до него из-за ошибки. После закрытия файлом пользоваться нельзя:
f = open("notes.txt", encoding="utf-8")
f.close()
f.read()
ValueError: I/O operation on closed file.
Граница: with гарантирует закрытие файла в программе, но не гарантирует, что при внезапном отключении питания данные уже физически на диске. Для этого служит os.fsync() — пример ниже.
Ошибки при открытии и как их обрабатывать
Неверно — открыть файл, не проверив, что он существует:
open("missing.txt", encoding="utf-8")
FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'
Исправление — перехватить конкретное исключение и решить, что делать без файла:
from pathlib import Path
path = Path("data") / "missing.txt"
try:
text = path.read_text(encoding="utf-8")
except FileNotFoundError:
print(f"Нет файла: {path}")
text = ""
print(repr(text))
Нет файла: data/missing.txt
''
Перехватывать лучше конкретные классы, а не голый except: FileNotFoundError, PermissionError (нет прав), IsADirectoryError (по пути лежит каталог). Все они — наследники OSError.
pathlib: современная работа с путями
Модуль pathlib дает объект Path, с которым пути собираются оператором / и не зависят от разделителя ОС. Для простых задач у него есть короткие методы:
from pathlib import Path
p = Path("report.txt")
p.write_text("итог: 42\n", encoding="utf-8")
print(p.exists(), p.stat().st_size, p.suffix)
print(p.read_text(encoding="utf-8"), end="")
new = p.rename("report-2026.txt")
print(new, Path("report.txt").exists())
True 13 .txt
итог: 42
report-2026.txt False
Размер 13 байт: «итог» в UTF-8 занимает 8 байт, плюс : 42 и \n. rename() возвращает новый путь. Граница: на Windows rename() падает, если файл с новым именем уже существует; для замены существующего файла на всех ОС используют Path.replace() или os.replace(). Для перебора файлов в каталоге есть Path(".").glob("*.txt").
Надежная и безопасная запись
Если программа упадет посреди записи в "w", на диске останется наполовину записанный файл. Прием атомарной замены: писать во временный файл рядом, сбросить его на диск и одним вызовом os.replace() подменить старый:
import os
import tempfile
from pathlib import Path
def save_atomic(path, text):
path = Path(path)
fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=path.name, suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(text)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, path)
except BaseException:
os.unlink(tmp)
raise
save_atomic("config.txt", "mode=prod\n")
print(Path("config.txt").read_text(encoding="utf-8"), end="")
Результат — mode=prod. Читатель файла видит либо старую версию, либо новую, но не обрывок. Учтите: mkstemp() создает файл с правами только для владельца, и после замены они сохранятся.
Второй риск — имя файла от пользователя. Строка вида ../../etc/passwd выводит путь за пределы рабочего каталога. Имя проверяют после приведения к абсолютному пути:
from pathlib import Path
BASE = Path("uploads").resolve()
BASE.mkdir(exist_ok=True)
def safe_path(name):
target = (BASE / name).resolve()
if not target.is_relative_to(BASE):
raise ValueError(f"выход за пределы каталога: {name}")
return target
print(safe_path("a.txt").name)
try:
safe_path("../../etc/passwd")
except ValueError as e:
print(e)
a.txt
выход за пределы каталога: ../../etc/passwd
Это базовая защита от обхода каталога, а не полная: в веб-приложении нужны еще ограничение размера, проверка типа содержимого, права доступа процесса и хранение загрузок вне публичного каталога сайта.
Если не получилось
| Симптом | Причина | Что сделать |
|---|---|---|
FileNotFoundError, хотя файл «рядом» |
относительный путь считается от текущего каталога запуска, а не от скрипта | Path(__file__).parent / "notes.txt" |
UnicodeDecodeError |
кодировка файла не совпадает с encoding |
открыть в rb, посмотреть байты, указать верную кодировку |
| файл оказался пустым | открыт в "w" вместо "a" или "r" |
дописывать в "a", защищаться режимом "x" |
| строки склеились | write()/writelines() не добавляют \n |
добавлять \n явно или print(..., file=f) |
второй read() вернул '' |
позиция уже в конце файла | f.seek(0) перед повторным чтением |
TypeError при записи |
в "wb" передана строка, в "w" — байты |
str для текстового режима, bytes для бинарного |
Выводы
- Файл открывают
open(путь, режим, encoding=...)и почти всегда внутриwith: так он закроется даже при ошибке. - Режим определяет судьбу данных:
"w"очищает файл сразу,"a"дописывает,"x"не даст перезаписать существующий. - В текстовом режиме
encodingлучше указывать явно — кодировка по умолчанию зависит от системы. - Текстовые файлы читают циклом
for line in f, большие бинарные — кускамиread(size); конец файла обозначает пустая строка. - Для путей удобен
pathlib, для важных файлов — атомарная запись через временный файл иos.replace().
Где применяется / связь с практикой
Освойте тему на практике
Файлы — основа большинства прикладных задач на Python: разбор логов, загрузка CSV перед анализом, конфигурации, выгрузки отчетов, кеши. На практике к базовым open() и with быстро добавляются модули csv и json, обработка ошибок и работа с путями в разных ОС. Если хотите системно пройти основы языка с практикой и разбором кода от преподавателей, посмотрите курс Python Developer. Basic. Разобраться в отдельных темах можно на бесплатных открытых уроках Otus.
FAQ
Нужно ли вызывать close(), если файл открыт через with?
Нет, with закроет файл сам при выходе из блока. Повторный close() не навредит, но и пользы не даст.
Как прочитать файл с конца, например последние строки лога?
Для небольших файлов достаточно f.readlines()[-10:]. Для больших удобнее collections.deque(f, maxlen=10): он пройдет файл построчно и оставит в памяти только последние 10 строк.
Чем os.path отличается от pathlib?
os.path работает со строками через функции, pathlib — с объектами Path и методами. Возможности во многом совпадают; для нового кода чаще выбирают pathlib.
Можно ли одновременно читать и писать в один файл?
Да, режимом r+, но позиция общая для чтения и записи, и перед сменой операции нужен seek(). В большинстве задач проще прочитать файл целиком, обработать данные и записать результат атомарно.



