JSON в Python: чтение и запись файлов, CSV, pathlib и кодировка UTF-8
Для работы с JSON в Python есть встроенный модуль json: json.load читает данные из открытого файла, json.dump записывает их в файл, а json.loads и json.dumps делают то же со строками. Файл открывают через with open(path, encoding="utf-8"), пути удобно строить с pathlib.Path, а таблицы читают модулем csv.
Работа с JSON в Python не требует сторонних библиотек: модуль json входит в стандартную библиотеку и превращает словари и списки в текст и обратно. Рядом с ним почти всегда нужны ещё три вещи: функция open с конструкцией with, модуль pathlib для путей и модуль csv для таблиц. Ниже — всё по порядку, с рабочим кодом и граблями, на которых спотыкаются чаще всего.
Функция open в Python: режимы и кодировка
Любой текстовый файл открывается встроенной функцией open(path, mode, encoding=...). Она возвращает файловый объект, у которого есть методы read, readline, write и который можно перебирать в цикле построчно.
| Режим | Что делает |
|---|---|
"r" | Чтение (по умолчанию). Файла нет — FileNotFoundError |
"w" | Запись. Файл создаётся или обнуляется сразу при открытии |
"a" | Дописать в конец, существующее содержимое не трогается |
"x" | Создать новый файл; если он уже есть — FileExistsError |
"b" | Добавка к режиму: работа с байтами ("rb", "wb") |
Зачем нужен with
Файл нужно закрывать: иначе данные могут не записаться на диск, а система держит дескриптор занятым. Конструкция 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:
for line in f:
print(line.rstrip("\n"))
Перебор файла в цикле читает его по одной строке и не загружает целиком в память, поэтому так можно обрабатывать и гигабайтные логи. Метод f.read() вернёт всё содержимое одной строкой, f.readlines() — список строк.
Как записать строку в файл и не стереть старое
Самая частая ошибка новичка — открыть файл в режиме "w", чтобы «дописать» строчку. Режим "w" очищает файл в момент открытия, ещё до первого write. Чтобы добавить запись в конец, используйте "a", а чтобы гарантированно не перезаписать чужой файл — "x".
with open("log.txt", "a", encoding="utf-8") as f:
f.write("новая запись\n")
Учтите, что write не добавляет перевод строки сам — "\n" пишется явно. Можно также использовать print(..., file=f): он поставит перенос за вас.
Кодировка UTF-8: почему появляются «кракозябры»
До Python 3.15 функция open() без параметра encoding брала кодировку системы. На Linux и macOS это UTF-8, а на Windows с русской локалью — cp1251. Скрипт, который нормально работал на сервере, на ноутбуке коллеги выводил «Р•РґР°» вместо «Еда» или падал с UnicodeDecodeError.
В Python 3.15 по PEP 686 режим UTF-8 включён по умолчанию: open() без encoding читает и пишет UTF-8 на всех ОС. Вернуть старое поведение можно переменной окружения PYTHONUTF8=0 или флагом -X utf8=0, а явно попросить системную кодировку — encoding="locale".
import sys
print(sys.flags.utf8_mode) # 1 в Python 3.15
open("legacy.txt", encoding="locale") # системная кодировка
open("old_excel.csv", encoding="cp1251") # файл, сохранённый в cp1251
Обратная сторона: старый CSV из Excel или скрипта на Windows, сохранённый в cp1251, теперь будет прочитан как UTF-8 и вызовет UnicodeDecodeError. Лечится так: прочитать с encoding="cp1251" и пересохранить в UTF-8. Хороший тон не меняется — указывайте encoding явно, тогда код одинаково ведёт себя на любой версии.
pathlib в Python: пути как объекты
Склеивать пути строками ("data" + "/" + name) неудобно и ломается на разных ОС. Модуль pathlib даёт класс Path, где пути соединяются оператором /, а у объекта есть понятные свойства.
from pathlib import Path
data = Path("data")
csv_path = data / "expenses.csv"
print(csv_path.name) # expenses.csv
print(csv_path.stem) # expenses
print(csv_path.suffix) # .csv
print(csv_path.parent) # data
print(csv_path.exists())
data.mkdir(parents=True, exist_ok=True)
(data / "hello.txt").write_text("Привет", encoding="utf-8")
print((data / "hello.txt").read_text(encoding="utf-8"))
for p in data.glob("*.csv"):
print(p)
Методы read_text и write_text заменяют связку open + with для небольших файлов. Объект Path можно передать и в обычный open, и в json, и в csv — все они его принимают. Путь относительно самого скрипта, а не текущей папки, получают так: Path(__file__).parent / "data".
В Python 3.14 у Path появились методы copy() и move(), так что для простого копирования больше не нужен shutil. В той же версии в стандартную библиотеку добавили модуль compression.zstd для сжатия Zstandard (PEP 784).
JSON в Python: load, dump, loads, dumps
JSON — текстовый формат обмена данными: его отдают API, в нём хранят конфиги и отчёты. Модуль json умеет две операции: сериализацию (Python-объект → JSON-текст) и десериализацию (текст → объект). Методов четыре, и путаница между ними — частый вопрос.
| Функция | Что делает |
|---|---|
json.dumps(obj) | Объект → строка JSON |
json.loads(s) | Строка JSON → объект |
json.dump(obj, f) | Объект → записать в открытый файл |
json.load(f) | Прочитать JSON из открытого файла |
Запомнить просто: буква s на конце означает string — работа со строкой; без неё — с файлом.
Соответствие типов
| Python | JSON |
|---|---|
dict | объект {} |
list, tuple | массив [] |
str | строка |
int, float | число |
True / False | true / false |
None | null |
Кортеж при обратном чтении станет списком, а множества, даты и Decimal модуль не знает и выдаст TypeError: Object of type ... is not JSON serializable.
Как сохранить JSON в файл
import json
from pathlib import Path
report = {
"month": "2026-10",
"total": 15230.5,
"categories": {"еда": 8200, "транспорт": 2100},
"tags": ["дом", "работа"],
}
path = Path("data/report.json")
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
Параметр ensure_ascii=False сохраняет кириллицу как есть. Без него вместо «еда» в файле окажется "еда" — это корректный JSON, но читать его человеку невозможно. indent=2 делает красивое форматирование с отступами.
Как прочитать JSON из файла
with open("data/report.json", encoding="utf-8") as f:
data = json.load(f)
print(data["total"]) # 15230.5
print(data["categories"]["еда"]) # 8200
Результат — обычный словарь или список, с ним работают привычными средствами. Если файл не JSON или оборван, будет исключение json.JSONDecodeError с номером строки и позиции — его стоит ловить, когда данные приходят извне.
Парсинг JSON-строки и красивый вывод
import json
raw = '{"name": "Анна", "skills": ["python", "sql"], "active": true}'
user = json.loads(raw)
print(user["skills"][0]) # python
print(json.dumps(user, ensure_ascii=False, indent=2, sort_keys=True))
sort_keys=True сортирует ключи при выводе — удобно, чтобы сравнивать файлы в git. Для быстрого форматирования файла из терминала есть встроенная утилита: python -m json.tool data/report.json.
Ответ HTTP API — тоже строка JSON. Библиотека requests разбирает её методом response.json(), а внутри это тот же json.loads.
Подвох: ключи JSON всегда строки
d = {1: "январь", 2: "февраль"}
back = json.loads(json.dumps(d))
print(back == d) # False
print(back) # {'1': 'январь', '2': 'февраль'}
# back[1] -> KeyError, нужно back["1"]
fixed = {int(k): v for k, v in back.items()}
В JSON ключи объекта могут быть только строками, поэтому числовые ключи после круга сериализации превращаются в строки. Приводите их обратно при чтении.
Свои типы: даты и Decimal
import json
from datetime import date
def default(obj):
if isinstance(obj, date):
return obj.isoformat()
raise TypeError(f"не умею сериализовать {type(obj).__name__}")
print(json.dumps({"day": date(2026, 10, 1)}, default=default))
# {"day": "2026-10-01"}
Функция в параметре default вызывается для каждого неизвестного объекта. Для сложных случаев можно унаследоваться от json.JSONEncoder и переопределить метод default.
Как дописать элемент в JSON-массив
JSON нельзя дописать в конец, как текстовый лог: файл нужно прочитать, изменить и записать целиком.
path = Path("data/items.json")
items = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
items.append({"id": len(items) + 1, "title": "новый"})
path.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")
Если записей много и они добавляются постоянно, удобнее формат JSON Lines: по одному JSON-объекту на строку, файл открывается в режиме "a".
CSV в Python: модуль csv
CSV — таблица в текстовом файле: строки — записи, значения разделены запятыми. Модуль csv правильно обрабатывает кавычки, запятые внутри значений и переносы, чего не сделает ручной split(",").
import csv
with open("data/expenses.csv", newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
total = 0.0
for row in reader:
total += float(row["amount"])
print(row["date"], row["category"], row["amount"])
print("Итого:", total)
csv.DictReader превращает каждую строку в словарь по заголовку файла. Все значения — строки: числа и даты приводите сами (float, int, date.fromisoformat). Файл с разделителем «;» (так сохраняет русский Excel) читают с параметром delimiter=";".
rows = [
{"date": "2026-10-01", "category": "еда", "amount": 450},
{"date": "2026-10-02", "category": "транспорт", "amount": 60},
]
with open("data/out.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["date", "category", "amount"])
writer.writeheader()
writer.writerows(rows)
При записи обязательно newline="": без него на Windows между строками появятся пустые. Если файл будут открывать в Excel, используйте encoding="utf-8-sig" — метка BOM помогает Excel распознать UTF-8.
От CSV к JSON: полный пример
import csv
import json
from collections import defaultdict
from pathlib import Path
src = Path("data/expenses.csv")
dst = Path("data/report.json")
by_category = defaultdict(float)
with src.open(newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
by_category[row["category"]] += float(row["amount"])
report = {"total": sum(by_category.values()), "categories": dict(by_category)}
dst.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(dst.read_text(encoding="utf-8"))
Это ядро любого скрипта-отчёта: вход в CSV, агрегация в словаре, выход в JSON.
Типичные ошибки
FileNotFoundError— путь относительный и считается от текущей папки запуска, а не от скрипта. ИспользуйтеPath(__file__).parent.- Файл обнулился — открыли в
"w"вместо"a". UnicodeDecodeError— файл в другой кодировке; укажите её явно (cp1251,utf-8-sig).TypeError: Object of type set is not JSON serializable— преобразуйте в список или передайтеdefault.KeyErrorпосле загрузки JSON — числовые ключи стали строками.- Свой файл назван
json.pyилиcsv.py— он перекрывает стандартный модуль, иimport jsonимпортирует его.
Итог
Для JSON в Python достаточно стандартной библиотеки: json.load/json.dump для файлов, loads/dumps для строк, ensure_ascii=False и indent для читаемого вывода. Файлы открывайте через with с явной кодировкой, пути стройте через pathlib, таблицы читайте csv.DictReader. В курсе по Python эта тема разобрана на практике: трекер расходов читает траты из CSV и сохраняет отчёт в JSON.
Частые вопросы
Чем json.load отличается от json.loads?
json.load читает JSON из открытого файлового объекта, а json.loads разбирает строку. Аналогично json.dump пишет в файл, а json.dumps возвращает строку.
Как сохранить русские буквы в JSON без \u-последовательностей?
Передайте ensure_ascii=False в json.dump или json.dumps и откройте файл с encoding="utf-8".
Нужно ли ставить библиотеку для работы с JSON в Python?
Нет, модуль json встроен в стандартную библиотеку, достаточно написать import json.
Можно ли хранить в JSON дату или множество?
Напрямую нет: их нужно преобразовать, например дату в строку ISO через isoformat, а множество в список, либо передать функцию в параметр default.
Как прочитать большой JSON, который не помещается в память?
Стандартный json.load читает файл целиком; для больших объёмов храните данные в формате JSON Lines и разбирайте построчно через json.loads.
Эта тема подробно разобрана с практикой в курсе «Python 3.15: от новичка до профи».
Перейти к курсу