Файлы и pathlib
open и with
from pathlib import Path
import tempfile
folder = Path(tempfile.mkdtemp())
path = folder / "notes.txt"
with open(path, "w", encoding="utf-8") as f: # запись (файл создаётся или очищается)
f.write("Первая строка\n")
f.write("Вторая строка\n")
print("третья строка", file=f) # print тоже умеет писать в файл
with open(path, encoding="utf-8") as f: # чтение (режим "r" по умолчанию)
print(repr(f.read()))
with open(path, "a", encoding="utf-8") as f: # дописать в конец
f.writelines(["четвёртая\n", "пятая\n"])
with open(path, encoding="utf-8") as f:
for number, line in enumerate(f, start=1): # построчно, лениво
print(number, line.rstrip("\n"))
with гарантирует, что файл закроется, а буфер запишется на диск — даже при исключении.
Режимы
| Режим | Смысл |
|---|---|
r |
чтение (по умолчанию); файла нет → FileNotFoundError |
w |
запись; файл очищается или создаётся |
a |
дописывание в конец |
x |
создать новый; если существует → FileExistsError |
+ |
чтение и запись (r+, w+) |
b |
бинарный режим (rb, wb) — байты вместо строк |
t |
текстовый (по умолчанию) |
Способы чтения
from pathlib import Path
import tempfile
path = Path(tempfile.mkdtemp()) / "data.txt"
path.write_text("один\nдва\nтри\n", encoding="utf-8")
with open(path, encoding="utf-8") as f:
print(repr(f.readline())) # одна строка
print(f.readlines()) # остальные строки списком
with open(path, encoding="utf-8") as f:
print(repr(f.read(6))) # первые 6 символов
print(f.tell()) # позиция в байтах (кириллица — 2 байта на букву)
f.seek(0) # вернуться в начало
print(f.read().splitlines())
| Способ | Память | Когда |
|---|---|---|
for line in f |
одна строка | большие файлы, логи — по умолчанию |
f.read() |
весь файл | маленькие файлы, конфиги |
f.readlines() |
весь файл списком | редко нужно |
f.read(n) |
n символов/байт | бинарные данные блоками |
Кодировки
Файл на диске — это байты. Текстовый режим декодирует их в строку по кодировке:
from pathlib import Path
import tempfile
path = Path(tempfile.mkdtemp()) / "ru.txt"
path.write_text("Привет", encoding="utf-8")
print(path.read_bytes()) # 12 байт: по 2 на букву
print(path.read_text(encoding="utf-8"))
try:
path.read_text(encoding="ascii")
except UnicodeDecodeError as e:
print("UnicodeDecodeError:", e.reason)
print(path.read_text(encoding="cp1251")) # не та кодировка — «кракозябры»
print(path.read_text(encoding="ascii", errors="replace"))
Всегда указывайте encoding
Без encoding Python берёт системную кодировку: UTF-8 на Linux/macOS, но на Windows часто cp1251. Код «работает у меня», но ломается у коллеги. В Python 3.15 UTF-8 станет кодировкой по умолчанию, а пока — явно encoding="utf-8".
Бинарные файлы
from pathlib import Path
import tempfile, hashlib
path = Path(tempfile.mkdtemp()) / "blob.bin"
path.write_bytes(bytes(range(256)) * 1000)
sha = hashlib.sha256()
with open(path, "rb") as f:
while chunk := f.read(64 * 1024): # блоками по 64 КБ — для файлов любого размера
sha.update(chunk)
print(path.stat().st_size, "байт, sha256 =", sha.hexdigest()[:16], "...")
pathlib — пути как объекты
pathlib.Path — современная замена os.path: пути склеиваются оператором /, работают на любой ОС.
from pathlib import Path, PurePosixPath, PureWindowsPath
p = Path("/home/anna/projects/shop/src/main.py")
print(p.name, p.stem, p.suffix) # main.py main .py
print(p.parent, p.parent.parent.name)
print(p.parts[:3])
print(p.with_suffix(".txt").name, p.with_name("app.py").name)
print(Path("data") / "2026" / "report.csv")
print(p.relative_to("/home/anna"))
print(PureWindowsPath(r"C:\Users\Anna\file.txt").parts)
print(Path.home(), Path.cwd())
from pathlib import Path
import tempfile
root = Path(tempfile.mkdtemp())
(root / "src" / "utils").mkdir(parents=True, exist_ok=True) # как mkdir -p
(root / "src" / "main.py").write_text("print('hi')\n", encoding="utf-8")
(root / "src" / "utils" / "text.py").write_text("", encoding="utf-8")
(root / "README.md").write_text("# Проект", encoding="utf-8")
print(sorted(p.name for p in root.iterdir())) # содержимое папки
print(sorted(p.name for p in root.glob("*.md"))) # шаблон в папке
print(sorted(str(p.relative_to(root)) for p in root.rglob("*.py"))) # рекурсивно
f = root / "src" / "main.py"
print(f.exists(), f.is_file(), f.is_dir(), f.stat().st_size)
f.rename(root / "src" / "app.py")
(root / "README.md").unlink() # удалить файл
print(sorted(str(p.relative_to(root)) for p in root.rglob("*")))
| os / os.path | pathlib |
|---|---|
os.path.join(a, b) |
Path(a) / b |
os.path.exists(p) |
p.exists() |
os.path.basename(p) |
p.name |
os.path.splitext(p)[1] |
p.suffix |
os.makedirs(p, exist_ok=True) |
p.mkdir(parents=True, exist_ok=True) |
os.listdir(p) |
p.iterdir() |
glob.glob("**/*.py", recursive=True) |
Path().rglob("*.py") |
open(p).read() |
p.read_text(encoding=...) |
Временные файлы
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as tmp: # папка удалится после блока
p = Path(tmp) / "report.csv"
p.write_text("a,b\n1,2\n", encoding="utf-8")
print(p.exists())
print(p.exists())
with tempfile.NamedTemporaryFile("w+", suffix=".txt", encoding="utf-8") as f:
f.write("временные данные")
f.seek(0)
print(f.name.endswith(".txt"), f.read())
Атомарная запись
Если программа упадёт посреди write, файл останется наполовину записанным. Надёжный приём — записать во временный файл рядом и атомарно переименовать:
import os, tempfile, json
from pathlib import Path
def atomic_write(path: Path, text: str):
fd, tmp = tempfile.mkstemp(dir=path.parent, 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
target = Path(tempfile.mkdtemp()) / "state.json"
atomic_write(target, json.dumps({"version": 2}))
print(target.read_text(encoding="utf-8"))
Безопасность путей
from pathlib import Path
UPLOADS = Path("/srv/uploads").resolve()
def safe_path(user_filename: str) -> Path:
candidate = (UPLOADS / user_filename).resolve()
if not candidate.is_relative_to(UPLOADS): # защита от ../../etc/passwd
raise PermissionError(f"выход за пределы папки: {user_filename}")
return candidate
print(safe_path("avatar.png"))
try:
safe_path("../../etc/passwd")
except PermissionError as e:
print("PermissionError:", e)
Никогда не подставляйте имя файла от пользователя в путь без проверки — это уязвимость path traversal.
Вопрос на собеседовании
«Как прочитать большой файл, который не помещается в память?» — Итерироваться по файловому объекту for line in f (построчно) или читать блоками f.read(chunk_size) в цикле; обрабатывать потоково через генераторы. Не использовать read() и readlines().
Итог
with open(path, mode, encoding="utf-8")— всегда сwithи кодировкой.- Режимы
r,w(очищает!),a,x,b; большие файлы —for line in fили блоками. pathlib.Path:/для склейки,.name,.suffix,.parent,glob/rglob,read_text/write_text.- Временные файлы —
tempfile, надёжная запись — временный файл +os.replace. - Проверяйте пути от пользователя (
resolve+is_relative_to).
Практика решено 0 из 5
Напишите count_lines(path): вернуть количество непустых строк в текстовом файле
(строки из одних пробелов тоже считаются пустыми). Читайте файл построчно, с encoding="utf-8".
Тест сам создаст файл во временной папке.
Какие тесты проверяют код (2)
import tempfile, pathlib
p = pathlib.Path(tempfile.mkdtemp()) / "a.txt"
p.write_text("первая\n\n \nвторая\nтретья", encoding="utf-8")
assert count_lines(p) == 3
import tempfile, pathlib
p = pathlib.Path(tempfile.mkdtemp()) / "empty.txt"
p.write_text("", encoding="utf-8")
assert count_lines(p) == 0
def count_lines(path):
with open(path, encoding="utf-8") as f:
return sum(1 for line in f if line.strip())
Напишите find_by_ext(folder, ext): вернуть отсортированный список имён файлов
(только имя, без папок) с расширением ext (например, ".py") во всей папке рекурсивно.
Используйте pathlib.Path.rglob.
Какие тесты проверяют код (1)
import tempfile
from pathlib import Path
root = Path(tempfile.mkdtemp())
for name in ["a.py", "b.txt", "src/c.py", "src/deep/d.py", "src/e.md"]:
(root / name).parent.mkdir(parents=True, exist_ok=True)
(root / name).write_text("x")
assert find_by_ext(root, ".py") == ["a.py", "c.py", "d.py"]
assert find_by_ext(str(root), ".md") == ["e.md"]
from pathlib import Path
def find_by_ext(folder, ext):
return sorted(p.name for p in Path(folder).rglob(f"*{ext}") if p.is_file())
Что делает режим открытия файла?
| чтение | запись с очисткой | дописывание в конец | создать, ошибка если есть | |
|---|---|---|---|---|
"r" |
||||
"w" |
||||
"a" |
||||
"x" |
Почему при открытии текстового файла стоит всегда указывать encoding="utf-8"?
Как правильно прочитать файл размером 20 ГБ, чтобы найти строки с ошибками?
Войдите, чтобы отмечать пройденные уроки.