Python: уроки, справочник и задачи

Запись и чтение JSON-файлов в Python

Раздел: Работа с файлами в Python · Обновлено 25.10.2026
Руки на клавиатуре ноутбука с открытым кодом
Фото: Marc Chouinard, CC0

JSON стал стандартным форматом для настроек, кэша, обмена данными между сервисами и выгрузок из API. В Python для работы с ним есть встроенный модуль json, и базовая запись или чтение файла занимает три строки. На практике сразу возникают вопросы: почему вместо русских букв в файле П, как сохранить дату, что делать с ошибкой Expecting value и как дописывать данные в файл, не перечитывая его целиком. Разберем все по шагам.

Как записать данные в JSON-файл

Для записи используют json.dump(obj, file). Файл открывают на запись в текстовом режиме и обязательно с явной кодировкой.

import json

user = {
    "name": "Анна",
    "age": 29,
    "skills": ["python", "sql"],
    "active": True,
    "manager": None,
}

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

Содержимое файла user.json:

{
  "name": "Анна",
  "age": 29,
  "skills": [
    "python",
    "sql"
  ],
  "active": true,
  "manager": null
}

Обратите внимание: True превратился в true, None в null, строки стоят в двойных кавычках. Это уже не Python-словарь, а текст в формате JSON. Режим "w" перезаписывает файл целиком, о дозаписи поговорим в разделе про JSON Lines.

Кириллица и параметр ensure_ascii

По умолчанию ensure_ascii=True, и все символы вне ASCII заменяются escape-последовательностями:

print(json.dumps({"name": "Привет"}))
# -> {"name": "Привет"}

print(json.dumps({"name": "Привет"}, ensure_ascii=False))
# -> {"name": "Привет"}

Оба варианта корректный JSON, и при загрузке получится одна и та же строка. Но файл с ensure_ascii=False можно читать глазами и он занимает меньше места. Если вы указываете ensure_ascii=False, файл обязательно открывайте с encoding="utf-8". Иначе на Windows Python возьмет системную кодировку cp1251 и либо испортит символы вне ее, либо выбросит UnicodeEncodeError на эмодзи. Подробнее о кодировках строк и байтов рассказано в статье про кодировку строки.

Форматирование: indent, sort_keys, separators

ПараметрЧто делаетКогда использовать
indent=2 или 4переносы строк и отступыконфиги и файлы, которые читают люди
indent="\t"отступ табуляциейесли так принято в проекте
sort_keys=Trueсортирует ключи словарейфайлы в git: стабильный порядок дает чистые диффы
separators=(",", ":")убирает пробелы после запятых и двоеточийкомпактная передача по сети, большие выгрузки
ensure_ascii=Falseпишет символы как естьпочти всегда для русскоязычных данных
default=funcсериализация нестандартных типовдаты, Decimal, свои классы
data = {"b": 1, "a": [1, 2]}
print(json.dumps(data, separators=(",", ":"), sort_keys=True))
# -> {"a":[1,2],"b":1}

Как прочитать JSON-файл

Чтение выполняет json.load(file). Результат это обычные объекты Python: словари, списки, строки и числа.

import json

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

print(user["name"], user["skills"][0])   # -> Анна python
print(type(user))                        # -> <class 'dict'>

Соответствие типов при чтении и записи:

JSONPython при чтенииPython при записи
object {}dictdict
array []listlist, tuple
stringstrstr
number (целое)intint
number (дробное)floatfloat
true / falseTrue / Falsebool
nullNoneNone

Из таблицы следуют две ловушки при "круговом" сохранении данных. Кортеж после записи и чтения становится списком. Ключи словаря в JSON всегда строки, поэтому числовые ключи превращаются в строковые:

scores = {1: "золото", 2: "серебро"}
restored = json.loads(json.dumps(scores))
print(restored)        # -> {'1': 'золото', '2': 'серебро'}
print(restored.get(1)) # -> None, ключ теперь "1"

Если исходные ключи были числами, восстановите их явно: {int(k): v for k, v in restored.items()}.

Короткий вариант через pathlib

from pathlib import Path

path = Path("settings.json")
settings = json.loads(path.read_text(encoding="utf-8"))
path.write_text(json.dumps(settings, ensure_ascii=False, indent=2), encoding="utf-8")

Для небольших файлов это удобнее, чем блок with. Общие правила открытия файлов, режимы и работа с контекстным менеджером описаны в материале о чтении и записи файлов.

Разница между dump и dumps, load и loads

Буква s на конце означает string. Функции без нее работают с файлом, функции с ней работают со строкой.

ФункцияВходВыход
json.dump(obj, f)объект Python и открытый файлпишет в файл, возвращает None
json.dumps(obj)объект Pythonстрока JSON
json.load(f)открытый файлобъект Python
json.loads(s)строка или байты JSONобъект Python

Частая ошибка новичков: json.load("user.json"). Функция ждет файловый объект, а не имя файла, и выбросит AttributeError: 'str' object has no attribute 'read'. Остальные возможности модуля, включая разбор строк из API, собраны в обзоре модуля json.

Сохранение datetime, Decimal и своих классов

Модуль знает только базовые типы. Попытка сохранить дату дает ошибку:

from datetime import datetime
json.dumps({"created": datetime(2026, 10, 25, 14, 30)})
# -> TypeError: Object of type datetime is not JSON serializable

Параметр default

Функция в default вызывается для каждого объекта, который модуль не умеет сериализовать. Она должна вернуть что-то сериализуемое или выбросить TypeError.

from datetime import date, datetime
from decimal import Decimal
from dataclasses import dataclass, asdict, is_dataclass
from pathlib import Path

def to_json(obj):
    if isinstance(obj, date):          # покрывает и datetime
        return obj.isoformat()
    if isinstance(obj, Decimal):
        return str(obj)                # строка, чтобы не потерять точность
    if isinstance(obj, (set, frozenset)):
        return sorted(obj)
    if isinstance(obj, Path):
        return str(obj)
    if is_dataclass(obj):
        return asdict(obj)
    raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")

@dataclass
class Order:
    id: int
    total: Decimal
    created: datetime

order = Order(7, Decimal("1999.90"), datetime(2026, 10, 25, 14, 30))
print(json.dumps(order, default=to_json, ensure_ascii=False))
# -> {"id": 7, "total": "1999.90", "created": "2026-10-25T14:30:00"}

Обратите внимание на последнюю строку функции. Если вместо исключения вернуть str(obj) для всего подряд, ошибки типов будут молча превращаться в строки вида "<object at 0x...>", и вы узнаете об этом только при чтении.

Обратное преобразование через object_hook

При чтении дата останется строкой. Чтобы получить объекты обратно, используйте object_hook: функцию, которую json.load вызывает для каждого прочитанного словаря.

def from_json(d):
    for key, value in d.items():
        if key in ("created", "updated") and isinstance(value, str):
            d[key] = datetime.fromisoformat(value)
    return d

raw = '{"id": 7, "created": "2026-10-25T14:30:00"}'
obj = json.loads(raw, object_hook=from_json)
print(obj["created"].year)   # -> 2026

Для денежных сумм удобен параметр parse_float=Decimal: все дробные числа сразу читаются как Decimal без потери точности. Для сложных моделей со вложенными типами и проверкой данных стоит посмотреть на Pydantic, который сериализует и валидирует JSON по описанию класса.

Как обновить JSON-файл безопасно

JSON нельзя изменить "на месте": нужно прочитать файл, поменять объект и записать заново. Если программа упадет во время записи, файл останется обрезанным и при следующем запуске не прочитается. Надежный прием: писать во временный файл рядом и атомарно заменять оригинал.

import json
import os
import tempfile
from pathlib import Path

def save_json(path, data):
    path = Path(path)
    fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        os.replace(tmp, path)          # атомарная замена
    except BaseException:
        os.unlink(tmp)
        raise

def load_json(path, default=None):
    try:
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return default

counters = load_json("stats.json", default={})
counters["visits"] = counters.get("visits", 0) + 1
save_json("stats.json", counters)

Функция os.replace заменяет файл за одну операцию файловой системы: читатель увидит либо старую, либо новую версию, но не половину.

JSON Lines: формат для логов и больших выгрузок

Обычный JSON-файл это один объект, и чтобы добавить запись в список, приходится перезаписывать весь файл. Формат JSON Lines (расширение .jsonl) хранит по одному JSON-объекту на строку. Его можно дописывать в конец и читать построчно, не загружая в память целиком.

import json

events = [
    {"user": "anna", "action": "login"},
    {"user": "boris", "action": "buy", "sum": 450},
]

# дозапись в конец
with open("events.jsonl", "a", encoding="utf-8") as f:
    for e in events:
        f.write(json.dumps(e, ensure_ascii=False) + "\n")

# построчное чтение
def read_jsonl(path):
    with open(path, encoding="utf-8") as f:
        for line_no, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            try:
                yield json.loads(line)
            except json.JSONDecodeError as e:
                print(f"строка {line_no} повреждена: {e.msg}")

total = sum(e.get("sum", 0) for e in read_jsonl("events.jsonl"))
print(total)   # -> 450

При записи JSON Lines нельзя использовать indent: каждая запись должна занимать ровно одну строку. Зато поврежденная строка ломает только одну запись, а не весь файл. Другие приемы обработки файлов на гигабайты описаны в статье про чтение больших файлов.

Ошибка JSONDecodeError и ее причины

json.JSONDecodeError это подкласс ValueError. У исключения есть полезные атрибуты: msg (описание), lineno и colno (строка и колонка), pos (позиция символа), doc (исходный текст).

СообщениеЧастая причинаРешение
Expecting value: line 1 column 1 (char 0)пустой файл или пустой ответ сервера, HTML вместо JSONпроверить размер файла и содержимое ответа
Expecting property name enclosed in double quotesодинарные кавычки, то есть сохранен str(dict)записывать через json.dump, а не str()
Unexpected UTF-8 BOMфайл сохранен в Блокноте с BOMоткрыть с encoding="utf-8-sig"
Extra dataв файле несколько объектов подрядэто JSON Lines, читать построчно
Ошибка про запятую в концезапятая после последнего элементаубрать лишнюю запятую, формат ее не допускает
try:
    with open("config.json", encoding="utf-8-sig") as f:
        config = json.load(f)
except FileNotFoundError:
    config = {}
except json.JSONDecodeError as e:
    raise SystemExit(f"config.json: {e.msg}, строка {e.lineno}, колонка {e.colno}")

Кодировка utf-8-sig читает файлы и с BOM, и без него, поэтому для конфигов, которые могут редактировать вручную на Windows, это безопасный выбор.

Отдельный случай: значения NaN и Infinity. Python по умолчанию записывает их в файл, но это нарушает стандарт, и парсеры других языков их отвергнут. Чтобы получить ошибку еще при записи, используйте allow_nan=False.

Типичные ошибки при работе с JSON-файлами

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

Открытие файла на запись до чтения

Конструкция, в которой файл открывается в режиме "w", а потом из него же пытаются прочитать старые данные, стирает содержимое сразу при открытии. Режим "w" обнуляет файл еще до первой записи. Сначала прочитайте данные и закройте файл, затем откройте его на запись.

Несколько вызовов dump в один файл

Если вызвать json.dump несколько раз в цикле, в файле окажутся склеенные объекты вида {...}{...}. Такой файл не читается через json.load, ошибка будет "Extra data". Либо собирайте все данные в один список и записывайте один раз, либо пишите в формате JSON Lines с переводом строки после каждого объекта.

Потеря точности чисел

Дробные числа при чтении становятся float, и сумма 0.1 + 0.2 превращается в 0.30000000000000004. Для денег это недопустимо. Храните суммы строками или в копейках целым числом, а при чтении используйте parse_float=Decimal. Обратная проблема: большие целые числа Python запишет точно, но JavaScript на стороне клиента округлит все, что больше 2 в степени 53. Идентификаторы такого размера безопаснее передавать строками.

Доверие к данным из файла

Модуль json безопасен в том смысле, что не выполняет код при чтении, в отличие от pickle или eval. Но структура данных может быть любой: ключа может не оказаться, вместо списка может прийти строка, а файл может весить гигабайты. Проверяйте типы и наличие ключей через dict.get и isinstance или описывайте схему данных классом с валидацией.

JSON или другой формат

JSON не всегда лучший выбор для хранения данных. Для конфигурации, которую редактирует человек, удобнее TOML: он допускает комментарии и проще читается. Для табличных данных с одинаковыми полями компактнее CSV, который открывается в табличных редакторах. Для внутреннего кэша Python-объектов быстрее pickle, но его нельзя загружать из недоверенных источников. Для большого объема данных с запросами и обновлениями отдельных записей лучше подходит SQLite. JSON остается оптимальным, когда данные иерархические, их читают программы на разных языках и объем умеренный.

Практический ориентир: если JSON-файл приложения вырос до десятков мегабайт и при каждом изменении одной записи вы перезаписываете его целиком, пора переходить на SQLite. Если же файл служит для обмена с внешним сервисом или хранит настройки на несколько десятков строк, JSON подходит идеально, и усложнять решение нет смысла. Для промежуточного варианта, когда записи только добавляются и редко читаются целиком, хорошо работает JSON Lines.

Короткий чек-лист

Частые вопросы

Как дописать данные в существующий JSON-файл?

Прочитать файл через json.load, добавить элемент в список или словарь и записать весь объект обратно. Открытие в режиме "a" и повторный json.dump создадут два объекта подряд, и файл перестанет читаться. Если дописывать нужно часто, переходите на JSON Lines.

Как прочитать очень большой JSON-файл?

json.load загружает весь документ в память, и объем памяти обычно в несколько раз больше размера файла. Для гигантских массивов используйте потоковый парсер, например пакет ijson, или конвертируйте данные в JSON Lines.

Почему нельзя сохранять словарь через str()?

str(dict) дает представление Python с одинарными кавычками, True и None. Это не JSON, и json.load его не прочитает. Для обмена данными всегда используйте json.dumps.

Сохраняется ли порядок ключей?

Да. Словари в Python сохраняют порядок вставки, json.dump записывает ключи в этом порядке, а json.load восстанавливает его. Изменить порядок можно параметром sort_keys=True.

Можно ли писать комментарии в JSON-файле?

Стандарт JSON комментарии не допускает, и json.load на них выдаст ошибку. Для конфигов с комментариями лучше подходит TOML: в Python 3.11+ его можно прочитать встроенным модулем tomllib.