Перейти к содержанию

192 уроков, 14 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 17 из 20 Средний 35 мин 120 XP

Сохраняем результаты: json.dump итоговых данных

Отфильтровали, отсортировали, посчитали — теперь результат должен пережить скрипт: json.dump пишет итог в файл с кириллицей и лесенкой.

Редакция Питоники

Два урока подряд ты обрабатывал ответ API: фильтр отобрал нужное, sorted навёл порядок. Есть одна проблема: всё это живёт в переменных и умирает вместе со скриптом. Завтра запустишь — и считать заново. Отчёты для того и существуют, чтобы переживать запуск: сегодня записываем итоги на диск.

Файл с результатами — это ещё и стык между программами: отчёт, который записал твой скрипт на Python, завтра откроет коллега на JavaScript, а послезавтра — скрипт на Go. JSON задуман как язык обмена, поэтому «сохранить результат» почти всегда означает «сохранить в json»: читают его все, типы в нём не врут, а кириллица при правильных параметрах остаётся кириллицей.

json.dump: итоговый список в файл

В уроке 4 вы уже записывали словарь в файл — здесь то же самое, только в файл едет не сырой ответ, а результат обработки. Схема одна: открыли файл на запись, json.dump(data, f, ...), закрыли — потом прочли обратно и убедились, что всё доехало.

выборка фильтра уходит в файл
import json, os

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]
cheap = [item for item in items if item["price"] < 5000]

with open("cheap.json", "w", encoding="utf-8") as f:
    json.dump(cheap, f, ensure_ascii=False, indent=2)

with open("cheap.json", encoding="utf-8") as f:
    back = json.load(f)

print(len(back), "товара в файле")
print(back[0]["name"], "-", back[0]["price"])
os.remove("cheap.json")
Вывод
3 товара в файле
Клавиатура - 4990

Два параметра записи — старые знакомые: ensure_ascii=False оставляет кириллицу кириллицей вместо \u-кракозябр (разбор — в уроке 5), а indent=2 рисует лесенку, которую приятно открыть глазами (урок 6). Файл удаляем в конце, чтобы блок можно было запускать сколько угодно раз — у себя на компьютере ты его, конечно, оставишь.

dumps и dump: текст или файл

Буква s в названии — строка: json.dumps возвращает текст, json.dump сразу пишет его в открытый файл. Под капотом dump вызывает тот же dumps — разница только в адресате.

dumps возвращает строку
import json

data = {"name": "Мышь", "price": 1290}

text = json.dumps(data, ensure_ascii=False)
print(text)
print(type(text).__name__)
Вывод
{"name": "Мышь", "price": 1290}
str

Строка от dumps годится для сообщений, логов и вставки в другой JSON; для файлов удобнее dump — не плодишь промежуточный текст. Оба параметра — ensure_ascii и indent — работают одинаково у обоих.

ФункцияКуда попадают данныеТипичный случай
json.dumps(data)возвращённая строкалоги, сообщения, вложение в другой ответ
json.dump(data, f)открытый файлотчёты, выгрузки, кэш
json.loads(text)из строки в Pythonразбор ответа API
json.load(f)из файла в Pythonчтение конфига и выгрузок

Для файлов, которые меняются от запуска к запуску и сравниваются глазами или diff-ом, есть ещё один привычный параметр — sort_keys=True: ключи выстраиваются по алфавиту, и отчёт от вторника отличается от отчёта со среды только содержимым, а не порядком ключей.

sort_keys: стабильный порядок ключей
import json

report = {"source": "shop-api", "items": ["Мышь", "Коврик"], "count": 2}

print(json.dumps(report, ensure_ascii=False))
print(json.dumps(report, ensure_ascii=False, sort_keys=True))
Вывод
{"source": "shop-api", "items": ["Мышь", "Коврик"], "count": 2}
{"count": 2, "items": ["Мышь", "Коврик"], "source": "shop-api"}

Не голый список, а отчёт

Список товаров в файле — уже хорошо, но через неделю никто не вспомнит, что это и когда посчитано. Заворачивай итог в словарь с метаданными: кто считал, сколько, по какому условию. Данные и справка о них поедут вместе.

отчёт с метаданными
import json, os

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]
cheap = [item for item in items if item["price"] < 5000]

report = {
    "generated_by": "shop-filter",
    "condition": "price < 5000",
    "count": len(cheap),
    "items": cheap,
}

with open("report.json", "w", encoding="utf-8") as f:
    json.dump(report, f, ensure_ascii=False, indent=2)

with open("report.json", encoding="utf-8") as f:
    back = json.load(f)

print(sorted(back.keys()))
print("Товаров:", back["count"])
os.remove("report.json")
Вывод
['condition', 'count', 'generated_by', 'items']
Товаров: 3

Ключ condition — настоящая находка для будущего тебя: файл сам рассказывает, по какому правилу собран. Такие метаданные ничего не стоят при записи и экономят часы при разборе чужих выгрузок. Заметь и форму: condition — строка с условием на человеческом языке, а не программа. Файл читает не только Python, и справка, понятная без интерпретатора, ценнее самой хитрой структуры.

Про indent=2 одна оговорка: лесенка раздувает файл — на десятках мегабайт данных отступы съедают заметную часть объёма. Поэтому правило простое: людям — indent, машинам — без. Отчёт для глаза и diff-а пишется с indent=2, а промежуточная передача между программами — сплошной строкой; json.loads читает оба вида одинаково.

Перезапись или дополнение

У json.dump нет режима «дописать». Каждый вызов json.dump пишет файл с нуля: старое содержимое исчезает, остаётся последняя версия данных. Это не баг, а правило формата: JSON-файл — одна целая структура, и половинки в нём не живут. Если накопить нужно — читай старый файл, изменяй данные в памяти и записывай всё обратно.

накопление: прочитал, изменил, записал
import json, os

PATH = "stats.json"

stats = {}
if os.path.exists(PATH):
    with open(PATH, encoding="utf-8") as f:
        stats = json.load(f)

stats["мыши"] = stats.get("мыши", 0) + 2
stats["клавиатуры"] = stats.get("клавиатуры", 0) + 1

with open(PATH, "w", encoding="utf-8") as f:
    json.dump(stats, f, ensure_ascii=False, indent=2)

print(stats)
os.remove(PATH)
Вывод
{'мыши': 2, 'клавиатуры': 1}

Проверка os.path.exists отличает первый запуск от последующих: файла ещё нет — начинаем с пустого словаря. stats.get(ключ, 0) подставляет ноль для товара, который раньше не встречался. Запусти такой код у себя два раза подряд — во второй прогон статистика придёт уже накопленная: три мыши и две клавиатуры. В песочнице файл удаляется, чтобы каждый запуск был честным первым — накопление легко проверить и так, читая файл между прогонами.

перезапись своими глазами
import json, os

PATH = "latest.json"

with open(PATH, "w", encoding="utf-8") as f:
    json.dump({"run": "первая"}, f, ensure_ascii=False)

with open(PATH, "w", encoding="utf-8") as f:
    json.dump({"run": "вторая"}, f, ensure_ascii=False)

with open(PATH, encoding="utf-8") as f:
    print(json.load(f))
os.remove(PATH)
Вывод
{'run': 'вторая'}

Первый dump не пережил второй: файл хранит то, что записано последним. Для отчётов это как раз удобно — имя файла стабильное, а содержимое всегда актуальное; исторические версии лучше складывать в файлы с датой в имени. Скрипт всегда можно запустить заново — данные пересоберутся и перезапишутся без страхов и бэкапов.

Проверка: прочитай обратно и сравни

Скрипт, который молча записал файл, ещё ничего не доказал: диск мог подвести, путь — оказаться не тем, кодировка — сработать против тебя. Дешёвая страховка — round-trip: сразу после записи прочитай файл json.load и сравни с тем, что собирался сохранить.

сохранил и проверил
import json, os

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]
result = sorted(
    (item for item in items if item["in_stock"]),
    key=lambda item: item["price"],
)

PATH = "available.json"
with open(PATH, "w", encoding="utf-8") as f:
    json.dump(result, f, ensure_ascii=False, indent=2)

with open(PATH, encoding="utf-8") as f:
    back = json.load(f)

print("Совпало:", back == result)
print("В файле:", len(back), "товара")
os.remove(PATH)
Вывод
Совпало: True
В файле: 3 товара

back == result сравнивает структуры по содержимому — порядок ключей внутри словарей не важен, важны значения. True значит: файл корректен и в нём ровно то, что ты хотел сохранить. Одна строка проверки — и отчёт перестаёт быть обещанием.

В файл можно уложить и сводку целиком — итог всех вопросов из урока 15: сколько всего, сколько в наличии, какая средняя. Числа в словаре едут как числа: json.dump сохраняет типы, и после обратного чтения avg_price можно снова делить и округлять без единого преобразования.

сводка каталога в файле
import json, os

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

summary = {
    "source": "shop-api",
    "count": len(items),
    "in_stock": len([i for i in items if i["in_stock"]]),
    "avg_price": round(sum(i["price"] for i in items) / len(items), 2),
}

PATH = "summary.json"
with open(PATH, "w", encoding="utf-8") as f:
    json.dump(summary, f, ensure_ascii=False, indent=2)

with open(PATH, encoding="utf-8") as f:
    print(json.load(f))
os.remove(PATH)
Вывод
{'source': 'shop-api', 'count': 5, 'in_stock': 3, 'avg_price': 6896.0}

Имя файла — тоже данные

result.json через неделю расскажет не больше, чем «какой-то результат». Имя — бесплатное место для метаданных, и в проектах его используют на полную: что за данные, какая выборка, когда собраны.

  • cheap-under-5000.json — что внутри и по какому условию;
  • catalog-fantasy.json — к какой категории относится;
  • report-2026-10-06.json — дата сборки: история версий сама себя ведёт;
  • items-shop-api.json — из какого источника приехали данные.

Дату в имя обычно ставит strftime из модуля datetime при каждом запуске — и папка отчётов превращается в архив по дням без единой лишней руки. А вот пробелы и кириллицу в именах лучше не пускать: они законны, но усложняют жизнь командной строке и чужим скриптам. Строчные латинские буквы, цифры, дефис — скучно и надёжно.

Чек-лист сохранения результата

  • открытие файла: режим "w" и encoding="utf-8" — обе части обязательны;
  • запись: json.dump(data, f, ensure_ascii=False, indent=2);
  • итог — словарь с метаданными: кто, когда, по какому условию считал;
  • проверка: json.load обратно и сравнение с исходными данными.
лёгкий экспорт: только нужные поля
import json, os

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

cheap = sorted(
    (item for item in items if item["price"] < 5000),
    key=lambda item: item["price"],
)
export = [{"name": i["name"], "price": i["price"]} for i in cheap]

PATH = "export.json"
with open(PATH, "w", encoding="utf-8") as f:
    json.dump(export, f, ensure_ascii=False, indent=2)

with open(PATH, encoding="utf-8") as f:
    back = json.load(f)

print(back == export, len(back))
print(back[0])
os.remove(PATH)
Вывод
True 3
{'name': 'Мышь', 'price': 1290}

Перед сохранением из товаров вырезаны лишние поля — получателю не нужен рейтинг и флаги, файл стал легче и понятнее. Формировать итоговую структуру под задачу получателя — такая же часть обработки, как фильтрация.

Что дальше

Результат обработки теперь переживает скрипт: отчёт с метаданными, кириллица без кракозябр, лесенка для человека и обратное чтение для уверенности. Но у JSON есть конкурент для табличных данных — CSV, родной формат Excel. Следующий урок сравнит оба формата и научит переводить данные туда и обратно.

Сохранённый отчёт — это данные плюс справка о себе: что, сколько, по какому условию и проверено ли обратным чтением.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

import json

data = {"price": 1290}
text = json.dumps(data)
print(text, type(text).__name__)
import json

saved = '{"n": 1}'
data = json.loads(saved)
data["n"] += 4
print(data)
import json

PATH = "stat.json"
with open(PATH, "w", encoding="utf-8") as f:
    json.dump({"clicks": 10}, f)
with open(PATH, "w", encoding="utf-8") as f:
    json.dump({"clicks": 25}, f)
with open(PATH, encoding="utf-8") as f:
    print(json.load(f)["clicks"])
Проверь себя
0 / 6

1. Чем json.dump отличается от json.dumps?

2. Что станет с существующим файлом после open(PATH, "w") и json.dump?

3. Как правильно «дополнить» json-файл новыми данными?

4. Зачем после записи читать файл обратно и сравнивать с исходными данными?

5. Почему для записи отчёта с кириллицей нужны и ensure_ascii=False, и encoding="utf-8"?

6. Что даёт заворачивание итогового списка в словарь с ключами generated_by и count?

Карточки терминов
Запомнено: 0 / 7
Практика

Собери выгрузку «склад»: отфильтруй товары в наличии, отсортируй их по цене по возрастанию и сохрани список в файл stock.json с ensure_ascii=False и indent=2. Прочитай файл обратно, выведи результат сравнения с исходным списком и количество товаров в файле.

practice.py
Вопросы и ответы по уроку

Как сохранить обработанные данные в json-файл в Python?

Открыть файл с режимом "w" и encoding="utf-8", вызвать json.dump(data, f, ensure_ascii=False, indent=2). ensure_ascii=False оставляет кириллицу читаемой, indent=2 делает файл удобным для человека. Затем прочитать файл json.load и сравнить с исходными данными — это проверка записи.

Почему нельзя дописывать в json-файл режимом «a»?

JSON-файл — одна целая структура: дописанный в конец текст ломает скобки, и первое же чтение падает с JSONDecodeError. Дополнение делают через чтение: json.load старого файла, обновление данных в памяти, json.dump всего обратно с "w".

Как проверить, что файл json записался правильно?

Round-trip: сразу после записи открыть файл и прочитать json.load, затем сравнить с исходной структурой: back == result. Сравнение проверяет содержимое, а не порядок ключей. Одна строка экономит часы поиска «пустого» или битого файла.

Чем json.dump лучше обычной записи файла через f.write?

f.write принимает только строки: словари и числа придётся вручную превращать в текст и экранировать. json.dump делает это сам и гарантирует корректный JSON: типы сохраняются, кириллица не ломается при ensure_ascii=False, а indent рисует лесенку.

Один большой json-файл или много маленьких?

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

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

Завернуть их в общий словарь: {"cheap": cheap, "available": available, "summary": summary} — и сделать один json.dump. JSON-файл хранит одну структуру, но у структуры может быть сколько угодно ключей; обратное чтение вернёт все списки по их именам. Отдельные json.dump в один файл не помогут — второй вызов перезапишет первый.

Понравился урок? Сошлитесь на него

«Каждый вызов json.dump пишет файл с нуля: старое содержимое исчезает, остаётся последняя версия данных.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.