Pythonjsonencodingunicode

Python JSON: русские буквы вместо текста коды

В json-файле вместо «кофе» лежит \u043a\u043e\u0444\u0435, и это не поломка, а поведение json.dump по умолчанию. Разбираем, за что отвечает ensure_ascii, за что encoding в open, и почему нужны обе настройки сразу.

7 мин чтенияСправочникPython · json · encoding · unicode · files

Файл не сломан: json.dump() по умолчанию экранирует всю кириллицу, поэтому вместо «кофе» на диск попадает \u043a\u043e\u0444\u0435. Чтобы буквы остались буквами, надо повернуть две ручки сразу — ensure_ascii=False в самом json.dump() и encoding="utf-8" в open(); любой из них по отдельности не хватит, и ниже видно, чем именно заканчивается каждый неполный вариант.

Почему в json-файле python пишет русские буквы вместо текста коды?

Потому что у json.dump() и json.dumps() есть параметр ensure_ascii, и по умолчанию он равен True. В этом режиме модуль обязан выдать чистый ASCII, поэтому каждый символ за пределами ASCII заменяется на escape-последовательность вида \uXXXX. Кириллица попадает под правило целиком: «товар» становится \u0442\u043e\u0432\u0430\u0440, «Москва» — \u041c\u043e\u0441\u043a\u0432\u0430. Это документированное поведение стандартной библиотеки, а не сбой кодировки и не баг в твоём коде.

Вот тот самый случай. Обрати внимание: encoding="utf-8" здесь уже указан — и он не помогает.

import json

coffee = {"товар": "кофе 250 г", "цена": 349, "склад": "Москва"}

with open("prices.json", "w", encoding="utf-8") as f:
    json.dump(coffee, f)

print(open("prices.json", encoding="utf-8").read())
{"\u0442\u043e\u0432\u0430\u0440": "\u043a\u043e\u0444\u0435 250 \u0433", "\u0446\u0435\u043d\u0430": 349, "\u0441\u043a\u043b\u0430\u0434": "\u041c\u043e\u0441\u043a\u0432\u0430"}

Такой файл не сломан: он читается обратно без потерь

Именно этот симптом люди ищут словами «python json русские буквы вместо текста коды» — и почти всегда сразу бегут его чинить. Сначала стоит убедиться, что чинишь ты именно баг. Юникод-последовательности \uXXXX — это часть спецификации JSON, а не мусор: любой корректный парсер обязан их развернуть. Проверь сам, скормив json.loads() ровно ту строку, которая лежит в файле.

import json

# ровно то, что json.dump() записал в файл без ensure_ascii
text = r'{"\u0442\u043e\u0432\u0430\u0440": "\u043a\u043e\u0444\u0435", "\u0446\u0435\u043d\u0430": 349}'

data = json.loads(text)
print(data)
print(data["товар"], data["цена"])
{'товар': 'кофе', 'цена': 349}
кофе 349

Строки вернулись целыми, ключи тоже. Значит, если файл читает только твоя же программа, менять вообще ничего не обязательно. Экранирование мешает ровно в двух местах: когда файл открывает человек глазами и когда его смотрит коллега в git-диффе. Оба повода уважительные — просто это вопрос удобства, а не корректности.

ensure_ascii=False отвечает за экранирование, а не за байты

Первая ручка живёт внутри модуля json и решает один-единственный вопрос: превращать ли не-ASCII символы в \uXXXX. К файлам и кодировкам она отношения не имеет — результат виден уже на строке в памяти, до всякой записи на диск.

import json

word = {"город": "Москва"}
print(json.dumps(word))
print(json.dumps(word, ensure_ascii=False))
{"\u0433\u043e\u0440\u043e\u0434": "\u041c\u043e\u0441\u043a\u0432\u0430"}
{"город": "Москва"}

Обе строки — обычные str, обе одинаково валидны как JSON, и обе разворачиваются в один и тот же словарь. Разница только в том, как текст выглядит. Базовый набор параметров сериализации собран в справочнике: JSON serialization.

encoding='utf-8' в open() отвечает за байты, а не за экранирование

Вторая ручка живёт вообще не в json, а в open(). Она решает, какими байтами твой текст ляжет на диск. Посмотри на файл в бинарном виде — там уже нет ни str, ни escape-последовательностей, только байты.

import json

word = {"город": "Москва"}

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

print(open("city.json", "rb").read())
b'{"\xd0\xb3\xd0\xbe\xd1\x80\xd0\xbe\xd0\xb4": "\xd0\x9c\xd0\xbe\xd1\x81\xd0\xba\xd0\xb2\xd0\xb0"}'

Каждая русская буква заняла два байта — это и есть UTF-8. Латиница, кавычки и двоеточия остались однобайтовыми. Подробнее про пару «символы против байтов» — в разделе Text encoding and decoding.

Четыре комбинации двух настроек: таблица симптомов

Почти все форумные ответы останавливаются на одной из трёх верхних строк. Тебе нужна четвёртая.

ensure_ascii encoding в open() Что окажется в файле Симптом
True (по умолчанию) не указан \u043a\u043e\u0444\u0435 Работает везде и никогда не падает, но глазами читать больно
True (по умолчанию) "utf-8" \u043a\u043e\u0444\u0435 Тот самый случай: кодировку указали, а коды остались
False не указан зависит от локали На английской Windows — UnicodeEncodeError, на русской — молчаливый файл в cp1251, на котором потом падает любой читатель, ждущий UTF-8
False "utf-8" кофе То, что нужно

Нужны ещё и отступы — добавляй третий параметр: json.dump(data, f, ensure_ascii=False, indent=2) при open(..., encoding="utf-8"). Это и есть полный рецепт «записать русские символы в файл через json.dump».

Почему при записи json на Windows появляется UnicodeEncodeError: charmap codec?

Потому что повернули только одну ручку из двух. Если у open() не указан encoding, Python берёт кодировку из локали операционной системы: на Linux и macOS это почти всегда UTF-8, а на Windows — однобайтовая кодовая страница. На английской или европейской Windows это cp1252, где кириллицы попросту нет, поэтому кодек падает на первой же русской букве. Пока ensure_ascii был включён, проблемы не возникало: escape-последовательности состоят из чистого ASCII и влезают в любую кодовую страницу.

Воспроизвести это можно на любой ОС, если указать cp1252 явно:

import json

coffee = {"товар": "кофе", "цена": 349}

try:
    with open("prices.json", "w", encoding="cp1252") as f:
        json.dump(coffee, f, ensure_ascii=False)
except UnicodeEncodeError as e:
    print(f"{type(e).__name__}: {e}")
UnicodeEncodeError: 'charmap' codec can't encode characters in position 1-5: character maps to <undefined>

Лечится одной правкой — encoding="utf-8" в open(). Узнать, какую кодировку подставляет твоя система, можно так: python -c "import locale; print(locale.getpreferredencoding(False))". Зеркальная беда случается при чтении: тот же перекос кодировок даёт либо UnicodeDecodeError, либо молчаливые кракозябры — это разобрано отдельно в статье про cp1251 и UTF-8 при чтении файлов.

Чем json.dump отличается от json.dumps и когда это важно?

json.dump() пишет прямо в файловый объект, а json.dumps() — с буквой s на конце, от string — возвращает готовую строку и никуда её не записывает. Параметр ensure_ascii у них одинаковый, а вот вторая ручка есть только у первого: кодировку задаёт тот open(), который ты передал в json.dump(). Если ты отдаёшь JSON в HTTP-ответ или кладёшь в очередь, файла нет вообще, остаётся только ensure_ascii, а за байты отвечает уже транспорт — например, заголовок Content-Type: application/json; charset=utf-8.

import json

user = {"имя": "Ольга", "город": "Казань"}

as_text = json.dumps(user, ensure_ascii=False)
print(type(as_text).__name__)
print(as_text)

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

print(open("user.json", encoding="utf-8").read())
str
{"имя": "Ольга", "город": "Казань"}
{"имя": "Ольга", "город": "Казань"}

Как загрузить json-файл с кириллицей обратно?

При чтении никаких специальных настроек у модуля json нет: ensure_ascii — параметр только записи, а json.load() одинаково разбирает и экранированные последовательности, и живые русские буквы. Явно указывать стоит только encoding в open() — по той же причине, что и при записи. Отдельный случай — чужой файл из Excel, 1С или выгрузки с сайта: в самом его начале может стоять невидимый BOM, и тогда разбор падает.

import json

# файл из чужой системы: перед { стоит невидимый BOM
with open("from_1c.json", "wb") as f:
    f.write('\ufeff{"склад": "Москва"}'.encode("utf-8"))

try:
    with open("from_1c.json", encoding="utf-8") as f:
        json.load(f)
except json.JSONDecodeError as e:
    print(e)

with open("from_1c.json", encoding="utf-8-sig") as f:
    print(json.load(f))
Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)
{'склад': 'Москва'}

Сообщение прямым текстом называет лекарство: encoding="utf-8-sig". При чтении этот кодек съедает BOM, если он есть, и ничего не меняет, если его нет, — поэтому для чужих входных файлов его можно ставить всегда. На записи он ведёт себя ровно наоборот: сам дописывает BOM в начало. Для своих json это лишнее — пиши с encoding="utf-8", иначе на твоём же файле споткнётся следующий json.load().

Когда экранирование всё-таки нужно

Иногда ensure_ascii=True — не помеха, а требование. Если JSON уезжает в старую систему, в лог с ASCII-фильтром, в поле базы с однобайтовой кодировкой или в канал, где никто не гарантирует UTF-8, экранированный вариант доедет без единой правки: в нём просто нет байтов старше 127.

import json

order = {"клиент": "Ольга", "сумма": 1290}

ascii_text = json.dumps(order)
utf8_text = json.dumps(order, ensure_ascii=False)

print(ascii_text.isascii(), utf8_text.isascii())
print(len(ascii_text), len(utf8_text))
print(json.loads(ascii_text) == json.loads(utf8_text))
True False
114 34
True

Цена вопроса видна в цифрах: экранированная запись втрое длиннее, зато состоит только из ASCII. И оба варианта разбираются в один и тот же словарь — выбор здесь про транспорт, а не про данные.

Частые ошибки

  1. Поставили ensure_ascii=False, но не тронули open(). Кириллица перестала экранироваться, и теперь её надо чем-то закодировать: на английской Windows получаешь UnicodeEncodeError, на русской молча получаешь cp1251-файл. Добавь encoding="utf-8".
  2. Поставили encoding="utf-8", но забыли ensure_ascii=False. Байты правильные, а в файле по-прежнему коды: экранирование случается раньше, и до кодировки живая кириллица просто не доезжает. Добавь ensure_ascii=False.
  3. Пробуют «расшифровать» файл через decode("unicode_escape"). На чисто ASCII-строке приём выглядит рабочим, но стоит в данных появиться настоящей кириллице — он молча превращает её в мусор, потому что читает UTF-8-байты как latin-1.
import json

user = {"имя": "Ольга", "город": "Казань"}
text = json.dumps(user, ensure_ascii=False)

print(repr(text.encode("utf-8").decode("unicode_escape")))
'{"имÑ\x8f": "Ð\x9eлÑ\x8cга", "гоÑ\x80од": "Ð\x9aазанÑ\x8c"}'

Единственный правильный распаковщик — json.loads().

  1. Пишут в файл str(словарь) вместо json.dump(). Русские буквы при этом выглядят прекрасно, а файл — не JSON: в нём одинарные кавычки.
import json

user = {"имя": "Ольга"}

with open("wrong.json", "w", encoding="utf-8") as f:
    f.write(str(user))

print(open("wrong.json", encoding="utf-8").read())

try:
    with open("wrong.json", encoding="utf-8") as f:
        json.load(f)
except json.JSONDecodeError as e:
    print(e)
{'имя': 'Ольга'}
Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
  1. Передают ensure_ascii в json.load(). Ответ будет однозначный: TypeError: JSONDecoder.__init__() got an unexpected keyword argument 'ensure_ascii'. Параметр существует только на записи.
  2. Открывают файл в бинарном режиме "wb" и отдают его в json.dump(). Получишь TypeError: a bytes-like object is required, not 'str': json.dump() пишет текст, значит режим должен быть "w" с указанной кодировкой.
  3. Файл записан правильно, а редактор всё равно показывает кракозябры. Тогда дело уже не в json, а в том, в какой кодировке файл открыли на просмотр.

Практика: сохрани и прочитай словарь с кириллицей без потерь

Итоговый рецепт — обе ручки плюс отступы, а затем проверка, что данные вернулись ровно теми же:

import json

catalog = [
    {"название": "кофе Арабика", "цена": 349.0},
    {"название": "чай Ассам", "цена": 219.5},
]

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

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

print(back == catalog)
print(back[0]["название"])
True
кофе Арабика

Заметь последнюю строку: back — это список словарей, поэтому обращение идёт через back[0]["название"], а не back["название"]. Промах на этом месте даёт одну из самых частых ошибок при работе с разобранным JSON — list indices must be integers or slices, not str.

Чтобы работа со словарями осела в руках, а не только в голове, порешай прямо в браузере: «Разобрать строку настроек» — превращение текста в словарь, ровно та операция, ради которой обычно и берут json; «Сложи одинаковые товары» — свёртка списка словарей, типичная форма данных из json-выгрузки. Код проверяет настоящий Python 3.12, ставить ничего не надо.

Мини-резюме

  • Коды вида \u043a\u043e\u0444\u0435 в файле — не поломка: это валидный JSON, и json.load() вернёт исходные строки без потерь.
  • ensure_ascii=False в json.dump() и json.dumps() отключает экранирование, но ничего не знает про байты.
  • encoding="utf-8" в open() задаёт байты, но ничего не знает про экранирование.
  • Нужны обе ручки: json.dump(data, f, ensure_ascii=False, indent=2) при open(path, "w", encoding="utf-8").
  • UnicodeEncodeError: 'charmap' codec означает, что вторую ручку забыли и кодировку выбрала локаль Windows.
  • При чтении хватает encoding="utf-8", а для чужих файлов с BOM — encoding="utf-8-sig".
  • Если JSON уезжает в ASCII-канал, оставляй ensure_ascii=True: файл длиннее, зато проходит везде.

Закрепи на практике

Решай задачи в Python-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр