Модуль 07 — Файлы, исключения, CSV и JSON

Данные, которые переживают перезапуск программы, и ошибки, которые не роняют её

Прогресс курса Модуль 7 из 9

Что вы освоите в этом модуле

01

Открытие и запись

open(путь, режим, encoding=...) открывает файл и возвращает объект файла. Через него читают и пишут, а после работы файл закрывают методом close: только тогда данные гарантированно записаны на диск.

РежимЧто делает
"r" (по умолчанию)Чтение; файла нет — FileNotFoundError
"w"Запись с нуля: существующий файл очищается
"a"Дописывание в конец
"x"Создать новый; если файл уже есть — ошибка, ничего не стирается
"rb", "wb"Двоичный режим: байты, без кодировки — для картинок, архивов
>>> f = open("notes.txt", "w", encoding="utf-8")
>>> f.write("Первая строка\n")
14
>>> f.write("Вторая строка\n")
14
>>> f.close()
>>> with open("notes.txt", "a", encoding="utf-8") as f:
...     f.write("Третья строка\n")
... 
14
>>> with open("notes.txt", encoding="utf-8") as f:
...     text = f.read()
... 
>>> text
'Первая строка\nВторая строка\nТретья строка\n'
>>> print(text, end="")
Первая строка
Вторая строка
Третья строка

write возвращает число записанных символов и сам перевод строки не добавляет — \n пишут явно. Конструкция with open(...) as f: закрывает файл сама, когда блок заканчивается, даже если внутри случилась ошибка. Пишите всегда так; ручной close в примере — только чтобы показать, что происходит.

02

Чтение по строкам

f.read() читает весь файл в одну строку — удобно для небольших файлов. Большой журнал читают по строкам циклом for line in f: в памяти в каждый момент только одна строка, и программа справится с файлом в гигабайты.

>>> with open("notes.txt", encoding="utf-8") as f:
...     for line in f:
...         print(repr(line))
... 
'Первая строка\n'
'Вторая строка\n'
'Третья строка\n'
>>> with open("notes.txt", encoding="utf-8") as f:
...     lines = [line.rstrip("\n") for line in f]
... 
>>> lines
['Первая строка', 'Вторая строка', 'Третья строка']
>>> open("nofile.txt", encoding="utf-8")
Traceback (most recent call last):
  File "<python-input-3>", line 1, in <module>
    open("nofile.txt", encoding="utf-8")
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'nofile.txt'

repr показывает, что каждая строка заканчивается \n. Его почти всегда убирают: line.rstrip("\n") или line.strip(), если и пробелы по краям не нужны. Открытие несуществующего файла — исключение FileNotFoundError; как его обработать — в разделе 05.

03

Кодировки

Файл на диске хранит байты, а не буквы. Кодировка — правило перевода букв в байты. Для русского текста встречаются две: UTF-8 — стандарт для Linux, веба и современных программ; cp1251 (Windows-1251) — старые программы Windows и выгрузки из учётных систем.

>>> with open("win.txt", "w", encoding="cp1251") as f:
...     f.write("Привет из Windows\n")
... 
18
>>> open("win.txt", encoding="utf-8").read()
Traceback (most recent call last):
  File "<python-input-1>", line 1, in <module>
    open("win.txt", encoding="utf-8").read()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "<frozen codecs>", line 325, in decode
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xcf in position 0: invalid continuation byte
>>> open("win.txt", encoding="cp1251").read()
'Привет из Windows\n'
>>> open("win.txt", "rb").read()
b'\xcf\xf0\xe8\xe2\xe5\xf2 \xe8\xe7 Windows\n'

Файл в cp1251, прочитанный как UTF-8, вызывает UnicodeDecodeError. Прочитанный в верной кодировке — нормальный текст. Байты \xcf\xf0… — та же фраза «как она лежит на диске»: в cp1251 каждая русская буква — один байт, в UTF-8 — два.

Всегда указывайте encoding="utf-8". Без него Python берёт кодировку системы: в Linux это UTF-8, а в Windows в версиях до 3.15 — cp1251. Программа, которая работала у вас, сломается на компьютере коллеги. Если источник присылает файлы в cp1251 — читайте с encoding="cp1251" и сохраняйте результат уже в UTF-8.
04

Пути: pathlib

Модуль pathlib представляет путь объектом Path. Части пути соединяются оператором / — и правильный разделитель подставляется сам: / в Linux, \ в Windows.

>>> from pathlib import Path
>>> p = Path("reports") / "2026" / "october.txt"
>>> p
PosixPath('reports/2026/october.txt')
>>> p.name, p.stem, p.suffix
('october.txt', 'october', '.txt')
>>> p.parent
PosixPath('reports/2026')
>>> p.exists()
False
>>> p.parent.mkdir(parents=True, exist_ok=True)
>>> p.write_text("Отчёт за октябрь\n", encoding="utf-8")
17
>>> p.exists()
True
>>> p.read_text(encoding="utf-8")
'Отчёт за октябрь\n'
>>> sorted(f.name for f in Path(".").glob("*.txt"))
['notes.txt', 'win.txt']
Что нужноКак
Имя, имя без расширения, расширение, родительp.name, p.stem, p.suffix, p.parent
Существует? Файл? Каталог?p.exists(), p.is_file(), p.is_dir()
Создать каталог со всеми родителямиp.mkdir(parents=True, exist_ok=True)
Прочитать / записать файл целикомp.read_text(encoding=…), p.write_text(текст, encoding=…)
Файлы по шаблону; во всех подкаталогахp.glob("*.txt"); p.rglob("*.txt")
Домашний каталог пользователяPath.home()

Относительный путь "reports" отсчитывается от текущего каталога — того, из которого запустили программу, а не того, где лежит файл .py. Если программа должна найти файл рядом с собой, путь строят от Path(__file__).parent.

05

Исключения: try и except

Исключение, которое никто не перехватил, останавливает программу. Блок try позволяет перехватить его и продолжить: если в try возникло исключение указанного типа, выполняется блок except.

>>> int("сорок")
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    int("сорок")
    ~~~^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'сорок'
>>> try:
...     n = int("сорок")
... except ValueError:
...     print("Это не число")
... 
Это не число
>>> try:
...     result = 10 / int("0")
... except ValueError:
...     print("не число")
... except ZeroDivisionError as e:
...     print("деление на ноль:", e)
... else:
...     print("результат", result)
... finally:
...     print("проверка закончена")
... 
деление на ноль: division by zero
проверка закончена

Блоков except может быть несколько — для разных типов. as e даёт доступ к самому исключению: print(e) выводит его текст. else выполняется, если исключения не было; finally — в любом случае, им освобождают ресурсы.

Главное применение — проверка ввода. Функция спрашивает, пока не получит правильное число:

def ask_int(prompt: str, low: int, high: int) -> int:
    """Спрашивает целое число в диапазоне, пока не введут правильное."""
    while True:
        text = input(prompt)
        try:
            value = int(text)
        except ValueError:
            print(f"  «{text}» — не целое число")
            continue
        if low <= value <= high:
            return value
        print(f"  нужно от {low} до {high}")


age = ask_int("Возраст: ", 14, 99)
print("Принято:", age)
$ python3 ask_int.py
Возраст: семнадцать
  «семнадцать» — не целое число
Возраст: 7
  нужно от 14 до 99
Возраст: 17.5
  «17.5» — не целое число
Возраст: 17
Принято: 17
Перехватывайте конкретный тип и только вокруг того кода, где ошибку можно исправить. except: без типа или except Exception: pass прячет любые ошибки — в том числе опечатки в вашем коде, — и программа молча делает не то.
06

raise: сообщить об ошибке

Функция, получившая неправильные данные, не должна печатать «ошибка» и возвращать что попало. Она выбрасывает исключение командой raise — а решает, что делать, вызывающий код:

def parse_score(text: str) -> int:
    """Балл 0–100 из строки; иначе ValueError с понятным текстом."""
    value = int(text)
    if not 0 <= value <= 100:
        raise ValueError(f"балл вне диапазона 0–100: {value}")
    return value


for raw in ["87", "120", "abc"]:
    try:
        print(raw, "->", parse_score(raw))
    except ValueError as e:
        print(raw, "-> ошибка:", e)
$ python3 raise_demo.py
87 -> 87
120 -> ошибка: балл вне диапазона 0–100: 120
abc -> ошибка: invalid literal for int() with base 10: 'abc'

ValueError — стандартный тип для «значение не подходит». Сообщение говорит, что именно не так и с каким значением: по такому тексту ошибку находят без отладчика. int("abc") внутри функции выбросил своё ValueError, и один и тот же except обработал оба случая.

07

CSV: таблицы

CSV — таблица в текстовом файле: строка файла — строка таблицы, значения разделены символом. Русские Excel и LibreOffice по умолчанию сохраняют CSV с разделителем ;, потому что запятая занята дробными числами. Ведомость students.csv:

ФИО;Группа;Тест 1;Тест 2;Проект
Анисимова Анна;21В;18;17;45
Борисов Борис;21В;12;;38
Волкова Вера;22Р;20;19;50
Глебов Глеб;22Р;9;11;н/я
Дмитриева Дарья;21В;15;16;41

В данных — обычные проблемы реальных таблиц: пустая ячейка у Борисова и «н/я» вместо числа у Глебова. Разбирать CSV через split(";") нельзя: значение в кавычках может содержать сам разделитель. Модуль csv делает это правильно. DictReader берёт первую строку как заголовки и выдаёт каждую строку словарём:

import csv

totals = {}
problems = []
with open("students.csv", encoding="utf-8", newline="") as f:
    reader = csv.DictReader(f, delimiter=";")
    for row in reader:
        try:
            score = sum(int(row[k] or 0) for k in ("Тест 1", "Тест 2", "Проект"))
        except ValueError:
            problems.append(row["ФИО"])
            continue
        totals[row["ФИО"]] = (row["Группа"], score)

with open("result.csv", "w", encoding="utf-8-sig", newline="") as f:
    writer = csv.writer(f, delimiter=";")
    writer.writerow(["ФИО", "Группа", "Итого"])
    for name, (group, score) in sorted(totals.items()):
        writer.writerow([name, group, score])

print("Записано строк:", len(totals))
print("Нужно проверить вручную:", ", ".join(problems))
$ python3 csv_report.py
Записано строк: 4
Нужно проверить вручную: Глебов Глеб
>>> print(open("result.csv", encoding="utf-8-sig").read(), end="")
ФИО;Группа;Итого
Анисимова Анна;21В;80
Борисов Борис;21В;50
Волкова Вера;22Р;89
Дмитриева Дарья;21В;72

Три настройки, без которых CSV ломается

  • newline="" при открытии — так требует модуль csv, иначе в Windows появляются пустые строки между записями.
  • delimiter=";" — для файлов из русского Excel и для файлов, которые в нём откроют.
  • encoding="utf-8-sig" при записи — UTF-8 с меткой BOM в начале файла. По ней Excel понимает, что файл в UTF-8; без неё кириллица в Excel откроется «кракозябрами».

row[k] or 0 превращает пустую ячейку в 0 (пустая строка ложна). «н/я» вызвал ValueError, и строка ушла в список на ручную проверку — программа не упала и не подставила молча ноль за неявку.

08

JSON: данные программы

JSON — текстовый формат для вложенных данных: объекты ({}), массивы ([]), строки, числа, true/false, null. Он почти совпадает с записью словарей и списков Python, читается человеком и понимается любым языком. В нём хранят настройки, данные небольших программ, ответы веб-сервисов.

>>> import json
>>> settings = {"город": "Санкт-Петербург", "порог": 55, "группы": ["21В", "22Р"], "архив": None}
>>> print(json.dumps(settings))
{"\u0433\u043e\u0440\u043e\u0434": "\u0421\u0430\u043d\u043a\u0442-\u041f\u0435\u0442\u0435\u0440\u0431\u0443\u0440\u0433", "\u043f\u043e\u0440\u043e\u0433": 55, "\u0433\u0440\u0443\u043f\u043f\u044b": ["21\u0412", "22\u0420"], "\u0430\u0440\u0445\u0438\u0432": null}
>>> print(json.dumps(settings, ensure_ascii=False, indent=2))
{
  "город": "Санкт-Петербург",
  "порог": 55,
  "группы": [
    "21В",
    "22Р"
  ],
  "архив": null
}
>>> with open("settings.json", "w", encoding="utf-8") as f:
...     json.dump(settings, f, ensure_ascii=False, indent=2)
... 
>>> with open("settings.json", encoding="utf-8") as f:
...     loaded = json.load(f)
... 
>>> loaded == settings
True
>>> loaded["группы"][1]
'22Р'
>>> try:
...     json.loads('{"порог": 55,}')
... except json.JSONDecodeError as e:
...     print("Ошибка JSON:", e)
... 
Ошибка JSON: Illegal trailing comma before end of object: line 1 column 13 (char 12)

dumps / loads работают со строкой, dump / load — с файлом. Без ensure_ascii=False кириллица записывается кодами г… — корректно, но нечитаемо. indent=2 раскладывает данные по строкам. JSON строже Python: только двойные кавычки, без запятой после последнего элемента, None пишется как null.

Программа заметок, которая хранит их между запусками:

"""Заметки с хранением в JSON."""
import json
import sys
from pathlib import Path

DB = Path("notes.json")


def load() -> list[str]:
    if not DB.exists():
        return []
    try:
        return json.loads(DB.read_text(encoding="utf-8"))
    except json.JSONDecodeError:
        sys.exit(f"Файл {DB} повреждён — исправьте или удалите его")


def save(notes: list[str]) -> None:
    DB.write_text(json.dumps(notes, ensure_ascii=False, indent=2), encoding="utf-8")


notes = load()
if len(sys.argv) > 1:
    notes.append(" ".join(sys.argv[1:]))
    save(notes)
for i, note in enumerate(notes, 1):
    print(f"{i}. {note}")
$ python3 notes_app.py Купить хлеб
1. Купить хлеб
$ python3 notes_app.py Сдать отчёт до пятницы
1. Купить хлеб
2. Сдать отчёт до пятницы
$ python3 notes_app.py
1. Купить хлеб
2. Сдать отчёт до пятницы

sys.argv — список аргументов командной строки, sys.argv[0] — имя программы (подробно — в модуле 08). sys.exit("текст") выводит текст в поток ошибок и завершает программу с кодом 1. Если файл повреждён — например, запись оборвалась на середине, — программа говорит об этом, а не показывает пользователю traceback:

$ python3 notes_app.py
Файл notes.json повреждён — исправьте или удалите его
[код 1]

Ключевые выводы модуля

Запомните главное

  • with open(путь, режим, encoding="utf-8") as f: — всегда с with и явной кодировкой; "w" стирает файл
  • Большие файлы читают по строкам: for line in f, затем line.rstrip("\n")
  • UnicodeDecodeError на русском тексте — файл в другой кодировке, обычно cp1251
  • Пути — pathlib.Path; относительный путь — от текущего каталога
  • try/except — только конкретный тип и только там, где ошибку можно исправить; свою ошибку — raise ValueError("что не так")
  • CSV — модуль csv, newline="", delimiter=";", для Excel utf-8-sig; JSON — ensure_ascii=False, indent=2
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Модуль 06: Функции и модули Все модули Модуль 08: Автоматизация