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

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

Начать обучение
Урок 16 из 20 Начальный 20 мин 120 XP

Словарь и JSON: json.loads и json.dumps

Словарь и JSON — два облика одних данных: json.loads читает строку в dict, json.dumps собирает строку из словаря, json.dump пишет в файл, а ensure_ascii=False бережёт кириллицу.

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

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

json.loads: строка в словарь

текст стал словарём
import json

raw = '{"city": "Сочи", "temp": 24}'
data = json.loads(raw)

print(data)
print(data['city'])
print(type(data).__name__)
Вывод
{'city': 'Сочи', 'temp': 24}
Сочи
dict

Смотрите на кавычки внимательно — их здесь два уровня. Питоновская строка raw обёрнута в одинарные кавычки, а внутри неё — JSON-текст с двойными: таковы правила формата, строки в JSON всегда в двойных кавычках. Функция json.loads прочитала этот текст и вернула настоящий словарь: type честно отвечает dict, доступ по ключу работает, и дальше это обычный словарь со всем, что мы про него знаем.

json.dumps: словарь в строку

словарь стал текстом
import json

book = {'title': 'Сказки', 'pages': 120}

text = json.dumps(book)
print(text)
print(type(text).__name__)
Вывод
{"title": "\u0421\u043a\u0430\u0437\u043a\u0438", "pages": 120}
str

Обратная дорога прошла, но взгляд цепляется за \u0421 и другие коды вместо «Сказки». Это не поломка: по умолчанию json.dumps кодирует все не-латинские буквы последовательностями \uXXXX, чтобы текст безопасно проходил через любые каналы и программы. Кракозябры — не поломка: данные не испорчены, и любой парсер развернёт коды обратно в кириллицу. Итог вызова — обычная строка: её можно записать в файл, отправить по сети, посчитать её длину через len.

ensure_ascii=False: кириллица остаётся

читаемая кириллица
import json

book = {'title': 'Сказки', 'pages': 120}

print(json.dumps(book, ensure_ascii=False))
Вывод
{"title": "Сказки", "pages": 120}

indent: вывод для человека

многострочный JSON
import json

book = {'title': 'Сказки', 'pages': 120, 'tags': ['детям', 'классика']}

print(json.dumps(book, ensure_ascii=False, indent=2))
Вывод
{
  "title": "Сказки",
  "pages": 120,
  "tags": [
    "детям",
    "классика"
  ]
}

Что происходит со списками и вложенными объектами

Многие новички думают, что JSON — это словарь и только. На самом деле у JSON два типа структур: объект (в Python — словарь) и массив (в Python — список). В ответе API они почти всегда идут вместе: сверху объект с метаданными, внутри — массив объектов. Разбирать такой ответ приходится в два шага, и словарь тут только верхний уровень.

объект с массивом внутри
import json

raw = '{"items": [{"id": 1}, {"id": 2}], "total": 2}'
data = json.loads(raw)

print(data['total'])
print(data['items'][0]['id'])
print(type(data).__name__)
Вывод
2
1
dict

Разбор по слоям. data — обычный словарь, верхний уровень ответа. data['items'] — список из двух словарей, каждый со своим ключом id. Дальше data['items'][0] даёт первый словарь в списке, а ['id'] — значение внутри него. Заметьте: тип data — dict, и никакой особый тип JsonResponse в Python не появляется. Дальше работают все обычные приёмы — get, перебор items(), сортировка.

Данные, которые JSON не умеет, тоже встречаются. Множество (set) и кортеж в JSON не поддерживаются: кортеж превратится в список, а множество вызовет ошибку. Если в словаре лежит datetime, dumps тоже споткнётся — для дат нужен либо default=str, либо ручное приведение к строке до сериализации.

множество JSON не переваривает
import json

try:
    print(json.dumps({'tags': {'a', 'b'}}))
except TypeError as e:
    print('TypeError:', e)
Вывод
TypeError: Object of type set is not JSON serializable
а кортеж превращается в список
import json

print(json.dumps({'point': (1, 2)}))
Вывод
{"point": [1, 2]}

Про кортеж стоит помнить: на выходе в JSON это уже список, и при обратном чтении вы получите не кортеж, а список. Данные «проехали туда-обратно» и сменили тип — типичная неожиданность, когда код ждёт кортеж, а получает список. Множество же не поедет вовсе: dumps честно откажется. Ошибка здесь лучше тихой подмены типа, но знать о ней надо заранее — особенно если в JSON кладут результат set() или zip().

round-trip: словарь проходит JSON и возвращается

Главное свойство JSON-текста: если он получен из словаря через dumps, то loads вернёт словарь, равный исходному. Это называется round-trip, и именно ради него формат и придуман: записали, передали по сети, прочитали — получили то же самое. Проверяется сравнением через ==, и словарь сравнивает пары без учёта порядка.

словарь — текст — словарь
import json

original = {'a': 1, 'b': [1, 2], 'c': {'d': 'текст'}}

text = json.dumps(original, ensure_ascii=False)
back = json.loads(text)

print(back)
print(back == original)
Вывод
{'a': 1, 'b': [1, 2], 'c': {'d': 'текст'}}
True

Равенство True — не мелочь, а рабочая проверка: если после round-trip что-то изменилось, значит, где-то по дороге данные потеряли тип или значение. Подвох в том, что == для словарей не проверяет порядок пар, — а вот для списков проверяет, и список после JSON остаётся списком. Ещё одна тонкость: ключи в JSON всегда строки, поэтому словарь с целочисленными ключами после round-trip потеряет их тип — {1: 'a'} превратится в {'1': 'a'}, и поиск по ключу 1 перестанет работать.

Словарь в файл и обратно

У модуля json есть пара без буквы s: dump и load. dump не возвращает строку, а пишет прямо в открытый файл, load читает из файла целиком. Разница между dumps и dump — в одном символе, а в использовании разница заметная: файлу настроек не нужна промежуточная строка в памяти.

запись словаря в файл
import json
import os

book = {'title': 'Сказки', 'pages': 120, 'author': 'Анна'}

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

with open('data.json', encoding='utf-8') as f:
    print(f.read())

os.remove('data.json')
print('файл удалён:', os.path.exists('data.json'))
Вывод
{
  "title": "Сказки",
  "pages": 120,
  "author": "Анна"
}
файл удалён: False

Разбор по строке. Блок with open(...) сам закрывает файл — даже если внутри случится ошибка, — поэтому руками закрывать не нужно. Кодировка utf-8 обязательна: без неё русский текст на Windows запишется в системной кодировке и прочитается обратно как мусор. Параметры ensure_ascii=False и indent=2 ушли прямо в dump — работают они точно так же, как у dumps. Последняя строка убирает файл, чтобы песочница не копилась: в браузере open пишет в виртуальную файловую систему Pyodide, файлы живут до перезагрузки страницы.

чтение файла обратно в словарь
import json
import os

book = {'title': 'Сказки', 'pages': 120, 'author': 'Анна'}

with open('data.json', 'w', encoding='utf-8') as f:
    json.dump(book, f, ensure_ascii=False)

with open('data.json', encoding='utf-8') as f:
    loaded = json.load(f)

print(loaded)
print(loaded == book)

os.remove('data.json')
Вывод
{'title': 'Сказки', 'pages': 120, 'author': 'Анна'}
True

Сравнение loaded == book даёт True, и это лучший тест для конфига: если после записи и чтения словарь изменился, значит, где-то потерялись данные или типы. Отдельный json.load читает файл целиком и сразу отдаёт словарь — никакой промежуточной строки. Смысла в json.load(open(...)) без блока with немного: файл останется открытым до конца программы, а на Windows это ещё и блокировка файла.

Когда JSONDecodeError — это подсказка

Ошибка парсинга почти всегда объяснима, если знать, где искать. JSONDecodeError сообщает три вещи: чего ожидала программа, на какой строке и в какой позиции это случилось. Почти всегда виновата одна из трёх причин: одинарные кавычки вместо двойных, Python-значения True/False/None вместо true/false/null или просто пустая строка.

парсер объясняет, чего не хватило
import json

try:
    json.loads("{'name': 'Барсик'}")
except json.JSONDecodeError as e:
    print('JSONDecodeError:', e)
Вывод
JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

Читать это сообщение полезно с конца. line 1 column 2 (char 1) — позиция в тексте: первый символ после открывающей фигурной скобки. Expecting property name enclosed in double quotes — чего не хватило: имени ключа в двойных кавычках. То есть дело не в значении, а в кавычках. Пустая строка даёт похожий текст: Expecting value: line 1 column 1 (char 0) — ждали значение, а нашли конец текста.

ЧтоВ PythonВ JSONЧто будет
Строка'текст'"текст"В JSON только двойные кавычки
ИстинаTruetruePython-значения JSON не понимает
ЛожьFalsefalseТо же самое
НичегоNonenullВ JSON именно null
Число42, 3.1442, 3.14Формат совпадает
Список[1, 2][1, 2]То же самое
Кортеж(1, 2)[1, 2]Кортеж станет списком
Множество{1, 2}—TypeError: not JSON serializable
Ключ словаря1 (число)"1" (строка)Ключ всегда строка

Что дальше

JSON разобрали — теперь словарь умеет ещё и сравниваться, копироваться и служить ключом. В уроке 19 про ==, copy и deepcopy — про то, почему два одинаковых словаря могут быть разными объектами. А если данные приходят не из файла, а из сети, и структура ответа незнакомая, — урок 17 про безопасный разбор ответа API через get.

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

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

import json

data = json.loads('{"a": 1}')
print(data['a'])
import json

print(json.dumps({'a': 1}))
import json

print(json.dumps({'чай': 1}, ensure_ascii=False))
Проверь себя
0 / 8

1. Что возвращает json.loads(строка)?

2. Что возвращает json.dumps(словарь)?

3. Почему в выводе json.dumps вместо русских букв стоят коды вида \u043c?

4. Какой параметр оставляет кириллицу как есть?

5. Парсер получил текст {'name': 'Барсик'} — с одинарными кавычками внутри. Что скажет json.loads?

6. Что произойдёт при json.dumps({'point': (1, 2)})?

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

8. Что будет, если словарь с целочисленными ключами пройдёт round-trip через JSON?

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

Дана строка order — JSON-запись заказа. Прочитай её через json.loads в словарь, выведи имя питомца по ключу 'name', затем собери из словаря JSON-строку с ensure_ascii=False и indent=2 и выведи её.

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

Как превратить словарь Python в JSON?

Функцией json.dumps: text = json.dumps(book, ensure_ascii=False, indent=2) вернёт JSON-строку с читаемой кириллицей и отступами. Для записи сразу в файл есть парная функция json.dump — она принимает файл вместо возврата строки.

Раздел про JSON с нуля

Как прочитать JSON в словарь Python?

Функцией json.loads: data = json.loads(строка) вернёт словарь, если в тексте объект JSON. Дальше это обычный dict: доступ по ключу, get, перебор items. Для чтения из файла есть json.load — ей передают открытый файл.

Почему после json.dumps вместо русских букв стоят \u043c и похожие коды?

По умолчанию dumps кодирует все не-латинские символы последовательностями \uXXXX — так текст безопасно проходит через любые каналы. Это валидный JSON, данные целы. Параметр ensure_ascii=False оставляет кириллицу как есть.

Урок 5 раздела json: кириллица

Чем JSON отличается от словаря Python?

Словарь — живая структура в памяти, JSON — текст по строгим правилам: строки только в двойных кавычках, True и None пишутся как true и null, ключи — только строки. Python-запись {'a': 1} JSON-парсер не примет — читайте и собирайте JSON только через модуль json.

Как записать словарь в файл как JSON?

Через json.dump: with open('data.json', 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2). Кодировку utf-8 указывайте обязательно — иначе русский текст на Windows запишется в системной кодировке. Обратно файл читается через json.load(f).

Как в JSON записать русский текст, чтобы его было видно?

Параметром ensure_ascii=False: json.dumps(data, ensure_ascii=False). По умолчанию все не-латинские символы кодируются последовательностями \uXXXX — это валидный JSON, но нечитаемый. Для машинной передачи данных разницы нет, для файлов и логов — есть.

Что будет, если в словаре лежит кортеж или множество?

Кортеж молча превратится в список: json.dumps({'point': (1, 2)}) даст {"point": [1, 2]}, и после обратного чтения тип уже не восстановится. Множество вызовет TypeError: Object of type set is not JSON serializable — JSON его просто не умеет.

Как прочитать JSON из строки, полученной от API?

Функцией json.loads: data = json.loads(response.text) вернёт словарь, если в тексте объект JSON. Дальше это обычный dict: доступ по ключу, get, перебор items. Если сервис отдаёт массив, loads вернёт список словарей, и разбор пойдёт в два шага — по индексу и по ключу.

Урок 17: разбор ответа API

Что означает ошибка Expecting property name enclosed in double quotes?

Что парсер нашёл не имя ключа в двойных кавычках. Чаще всего дело в том, что текст собран вручную с одинарными кавычками — {'name': 'Барсик'} для json.loads не JSON. Второй по частоте случай — Python-значения True, False, None вместо true, false, null.

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

«Кракозябры — не поломка: данные не испорчены, и любой парсер развернёт коды обратно в кириллицу.»

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

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

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

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