JSON: чтение, запись и сохранение данных
- Объяснить, что такое JSON и почему он лучше «свободного» текстового файла для сохранения данных
- Знать соответствие между типами JSON и типами Python
- Сохранять и загружать данные из файла с помощью
json.dumpиjson.load - Преобразовывать между строкой JSON и объектом Python с помощью
json.dumpsиjson.loadsи обрабатывать отсутствующий или испорченный файл
Что мы изучим?
- Что такое JSON и как он выглядит.
- Как JSON соответствует dict и list в Python.
- Сохранение в файл и загрузку из него.
- Преобразование между строкой JSON и данными.
Зачем это нужно?
В предыдущем уроке мы сохраняли текст и для этого придумали собственный «формат»: Dana,90 в каждой строке. А теперь допустим, что у каждого студента есть список оценок, адрес с городом и улицей и флаг is_active. Как сохранить это в строке текста? Каждый придумает свой формат, и придётся писать сложный код разбора.
JSON — стандартный формат для структурированных данных. Его умеет читать и писать любой язык программирования. И главное:
- Тема ООП — проект будет сохраняться в JSON.
- Темы бэкенда и баз данных — каждый запрос и ответ в API отправляется в формате JSON. FastAPI автоматически превращает dict Python в JSON и обратно.
Объяснение с основ
Что такое JSON?
JSON = JavaScript Object Notation. Он родился в JavaScript, но сегодня это общий формат. Файл JSON — текстовый файл с определённой структурой, который выглядит почти так же, как dict и list в Python:
{
"name": "Dana",
"age": 24,
"is_active": true,
"grades": [90, 85, 100],
"address": {"city": "Haifa", "street": "Herzl 10"},
"phone": null
}Соответствие между JSON и Python
| JSON | Python |
|---|---|
object { } | dict |
array [ ] | list |
string "text" | str |
number 42, 3.5 | int, float |
true / false | True / False |
null | None |
Важные правила JSON (отличаются от Python!)
- Строки и ключи — только в двойных кавычках
"...". Одинарные кавычки'...'недопустимы. true,false,null— строчными буквами.- Без запятой после последнего элемента.
- Без комментариев.
- Ключи объекта — всегда строки.
Сериализация и десериализация
| Термин | Направление | Функция |
|---|---|---|
| Сериализация (Serialization) | Данные Python → текст JSON | json.dump / json.dumps |
| Десериализация (Deserialization) | Текст JSON → данные Python | json.load / json.loads |
Как запомнить: s в конце (dumps, loads) = string, работа со строкой. Без s (dump, load) = работа с файлом.
Диван в гостиной (dict в памяти) не пройдёт в дверь как есть. Сериализация = разобрать его и упаковать в плоскую коробку с инструкцией по сборке (текст JSON), которую можно хранить на складе (в файле) или отправить грузовиком (по сети). Десериализация = собрать его снова в новом доме и получить точно такой же диван.
Синтаксис
import json
data = {"name": "Dana", "grades": [90, 85]}
# file
with open(path, "w", encoding="utf-8") as file:
json.dump(data, file, indent=2, ensure_ascii=False) # save
with open(path, "r", encoding="utf-8") as file:
data = json.load(file) # load
# string
text = json.dumps(data) # dict -> JSON string
data = json.loads(text) # JSON string -> dictindent=2— записывает «красивый» JSON с переводами строк и отступами. Без него всё в одной строке.ensure_ascii=False— неанглийский текст (например, русский или иврит) сохраняется как есть, а не в виде escape-кодов (обратная косая черта,uи четыре шестнадцатеричные цифры на каждую букву).
Первый пример кода
week-2/files/save_profile.py:
import json
from pathlib import Path
PROFILE_FILE = Path(__file__).parent / "profile.json"
profile = {
"name": "Dana",
"age": 24,
"is_active": True,
"grades": [90, 85, 100],
"address": {"city": "Haifa", "street": "Herzl 10"},
"phone": None,
}
with open(PROFILE_FILE, "w", encoding="utf-8") as file:
json.dump(profile, file, indent=2, ensure_ascii=False)
with open(PROFILE_FILE, "r", encoding="utf-8") as file:
loaded = json.load(file)
print("Loaded type:", type(loaded))
print("City:", loaded["address"]["city"])
print("Average:", sum(loaded["grades"]) / len(loaded["grades"]))
print("Same as original?", loaded == profile)Вывод:
Loaded type: <class 'dict'>
City: Haifa
Average: 91.66666666666667
Same as original? TrueСозданный profile.json:
{
"name": "Dana",
"age": 24,
"is_active": true,
"grades": [
90,
85,
100
],
"address": {
"city": "Haifa",
"street": "Herzl 10"
},
"phone": null
}Разбор кода построчно
| Строка | Объяснение |
|---|---|
import json | Стандартный модуль. Установка не нужна. |
json.dump(profile, file, indent=2, ...) | Сериализация: превращает dict в текст JSON и записывает его в файл. True стало true, а None — null. |
json.load(file) | Десериализация: читает текст и строит из него новый dict. |
type(loaded) | Совершенно обычный dict. С ним можно работать, как с любым словарём. |
loaded["address"]["city"] | Доступ к вложенной структуре, как в теме структур данных и функций. |
loaded == profile | True: данные полностью пережили «путешествие» в файл и обратно. (Но loaded is profile — False, потому что это новый объект.) |
Ещё один пример
Маленькая «база данных» в файле JSON — загрузка со значением по умолчанию, добавление, сохранение и обработка испорченного файла. Именно этот шаблон будет использоваться в проекте ООП. week-2/files/contacts_db.py:
import json
import logging
from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
DB_FILE = Path(__file__).parent / "contacts.json"
def load_contacts():
if not DB_FILE.exists():
logging.info("No database file yet - starting empty")
return []
try:
with open(DB_FILE, "r", encoding="utf-8") as file:
return json.load(file)
except json.JSONDecodeError:
logging.error("Database file is corrupted - starting empty")
return []
def save_contacts(contacts):
with open(DB_FILE, "w", encoding="utf-8") as file:
json.dump(contacts, file, indent=2, ensure_ascii=False)
logging.info("Saved %s contacts", len(contacts))
def add_contact(contacts, name, phone):
contact = {"id": len(contacts) + 1, "name": name, "phone": phone}
contacts.append(contact)
return contact
def main():
contacts = load_contacts()
new_contact = add_contact(contacts, "Dana", "050-1234567")
print("Added:", new_contact)
save_contacts(contacts)
print("Total contacts in file:", len(contacts))
if __name__ == "__main__":
main()Первый запуск:
INFO No database file yet - starting empty
Added: {'id': 1, 'name': 'Dana', 'phone': '050-1234567'}
INFO Saved 1 contacts
Total contacts in file: 1Второй запуск:
Added: {'id': 2, 'name': 'Dana', 'phone': '050-1234567'}
INFO Saved 2 contacts
Total contacts in file: 2Разбор:
- Файл хранит список словарей: ровно «таблицу» записей.
load_contactsобрабатывает два случая: файла нет (первый запуск) → пустой список. Файл испорчен (json.JSONDecodeError) → запись ошибки в лог и пустой список. В любом случае программа не падает."id": len(contacts) + 1— простой уникальный идентификатор. (Обратите внимание: если контакт удалить, следующий идентификатор может повторить существующий. В настоящей базе данных в темах бэкенда и баз данных база сама выдаёт идентификаторы, и это как раз одна из причин её использовать.)- Загрузить → изменить в памяти → сохранить: базовый шаблон работы с файлом данных.
Строка JSON — dumps / loads
Файл есть не всегда. В сети, например, вы получаете строку JSON:
import json
response_text = '{"id": 7, "name": "Dana", "active": true, "tags": ["new", "vip"]}'
student = json.loads(response_text)
print(student["name"], student["active"], student["tags"][1])
reply = {"status": "ok", "count": 3, "error": None}
print(json.dumps(reply))Dana True vip
{"status": "ok", "count": 3, "error": null}Именно это FastAPI будет делать за нас в темах бэкенда и баз данных: loads для входящего запроса и dumps для исходящего ответа.
Типичная ошибка
Ошибка 1: недопустимый JSON, написанный вручную.
json.loads("{'name': 'Dana'}")json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)В JSON — только двойные кавычки. Частая причина: скопировали вывод print(dict) из Python, где используются одинарные кавычки, поэтому это не допустимый JSON. Правильный способ создать JSON — всегда json.dumps, а не str(dict).
Ошибка 2: путать dump и dumps.
with open(path, "w", encoding="utf-8") as file:
json.dumps(data, file)TypeError: dumps() takes 1 positional argument but 2 were givenДля файла — dump (без s). dumps возвращает строку и никуда не пишет.
Ошибка 3: сохранять то, чего JSON не знает.
json.dumps({"tags": {"a", "b"}})TypeError: Object of type set is not JSON serializableВ JSON нет множества (и нет tuple: tuple сохраняется как массив и загружается обратно как list). Сначала преобразуйте: list(my_set). То же самое произойдёт в теме ООП с объектами классов, и там мы научимся превращать их в dict.
Ошибка 4: числовые ключи возвращаются строками.
data = {1: "one"}
loaded = json.loads(json.dumps(data))
print(loaded) # {'1': 'one'}
print(loaded[1]) # KeyError: 1В JSON ключи всегда строки, поэтому 1 стало "1". Обращайтесь через loaded["1"] или заранее планируйте текстовые ключи.
Практика
Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.
Упражнение 1: Из Python в JSON
Для каждого значения Python — как оно будет выглядеть в JSON?
True None "Dana" [1, 2] {"age": 24}Показать решение
| Python | JSON |
|---|---|
True | true |
None | null |
"Dana" | "Dana" — всегда двойные кавычки |
[1, 2] | [1, 2] — массив (array) |
{"age": 24} | {"age": 24} — объект (object); ключи всегда строки |
Упражнение 2: Сохранение и загрузка
Сохраните словарь в файл profile.json (с отступами и без кодирования неанглийского текста), загрузите его обратно и напечатайте имя и самую высокую оценку.
profile = {"name": "Dana", "grades": [90, 85, 100]}Ожидаемый вывод:
Dana
100Показать решение
import json
profile = {"name": "Dana", "grades": [90, 85, 100]}
with open("profile.json", "w", encoding="utf-8") as file:
json.dump(profile, file, indent=2, ensure_ascii=False)
with open("profile.json", "r", encoding="utf-8") as file:
loaded = json.load(file)
print(loaded["name"])
print(max(loaded["grades"]))Dana
100dump/load— с файлом.indent=2— файл, удобный для чтения людьми.ensure_ascii=False— неанглийский текст сохраняется как есть.
Упражнение 3: Строка JSON
Следующий ответ пришёл от сервера в виде строки. Преобразуйте его в словарь и напечатайте имя и активен ли студент. Затем преобразуйте словарь reply в строку JSON.
text = '{"id": 7, "name": "Dana", "active": true}'
reply = {"status": "ok", "error": None}Ожидаемый вывод:
Dana True
{"status": "ok", "error": null}Показать решение
import json
text = '{"id": 7, "name": "Dana", "active": true}'
student = json.loads(text)
print(student["name"], student["active"])
reply = {"status": "ok", "error": None}
print(json.dumps(reply))Dana True
{"status": "ok", "error": null}loads / dumps (с s — string) работают со строкой, а не с файлом. Так выглядит общение с сервером.
Упражнение 4: Настройки, которые сохраняются
Напишите load_settings(path), которая возвращает значения по умолчанию {"theme": "light", "font_size": 14}, а если файл существует — обновляет их согласно сохранённому в нём. Напишите также save_settings(path, settings).
print(load_settings("settings.json"))
save_settings("settings.json", {"theme": "dark"})
print(load_settings("settings.json"))Ожидаемый вывод:
{'theme': 'light', 'font_size': 14}
{'theme': 'dark', 'font_size': 14}Показать решение
import json
from pathlib import Path
DEFAULTS = {"theme": "light", "font_size": 14}
def load_settings(path):
settings = DEFAULTS.copy()
if Path(path).exists():
with open(path, "r", encoding="utf-8") as file:
saved = json.load(file)
for key, value in saved.items():
settings[key] = value
return settings
def save_settings(path, settings):
with open(path, "w", encoding="utf-8") as file:
json.dump(settings, file, indent=2)
print(load_settings("settings.json"))
save_settings("settings.json", {"theme": "dark"})
print(load_settings("settings.json")){'theme': 'light', 'font_size': 14}
{'theme': 'dark', 'font_size': 14}DEFAULTS.copy() — чтобы не менять сами значения по умолчанию (помните ссылки из урока об изменяемости?).
Упражнение 5: Список контактов
Напишите две функции: add_contact(path, name, phone), которая добавляет контакт в список, сохранённый в файле JSON (и создаёт файл, если его нет), и find_phone(path, name), которая возвращает телефон или None.
add_contact("contacts.json", "Dana", "050-1111111")
add_contact("contacts.json", "Avi", "052-2222222")
print(find_phone("contacts.json", "Avi"))
print(find_phone("contacts.json", "Noa"))Ожидаемый вывод:
052-2222222
NoneПоказать решение
import json
from pathlib import Path
def load_contacts(path):
if not Path(path).exists():
return []
with open(path, "r", encoding="utf-8") as file:
return json.load(file)
def add_contact(path, name, phone):
contacts = load_contacts(path)
contacts.append({"name": name, "phone": phone})
with open(path, "w", encoding="utf-8") as file:
json.dump(contacts, file, indent=2, ensure_ascii=False)
def find_phone(path, name):
for contact in load_contacts(path):
if contact["name"] == name:
return contact["phone"]
return None
add_contact("contacts.json", "Dana", "050-1111111")
add_contact("contacts.json", "Avi", "052-2222222")
print(find_phone("contacts.json", "Avi"))
print(find_phone("contacts.json", "Noa"))052-2222222
NoneФайл хранит список словарей — ровно структуру из урока о dict. Каждое изменение: загрузить, изменить, сохранить.
Итоги
- JSON = стандартный текстовый формат для структурированных данных. object ↔
dict, array ↔list,true/false/null↔True/False/None. - Правила: только двойные кавычки, без запятой в конце, без комментариев. Ключи — всегда строки.
- Файл:
json.dump(data, file)/json.load(file). Строка:json.dumps(data)/json.loads(text). indent=2— для читаемости,ensure_ascii=False— для неанглийского текста.- Шаблон: загрузить (со значением по умолчанию) → изменить в памяти → сохранить. Обрабатывайте отсутствующий файл и
JSONDecodeError. - Множества и объекты нужно преобразовать перед сохранением. JSON — язык каждого API в темах бэкенда и баз данных.