# JSON в Python: чтение и запись файлов, CSV, pathlib и кодировка UTF-8

URL: https://skillaria.top/ru/guides/python/json-python

Для работы с JSON в Python есть встроенный модуль json: json.load читает данные из открытого файла, json.dump записывает их в файл, а json.loads и json.dumps делают то же со строками. Файл открывают через with open(path, encoding="utf-8"), пути удобно строить с pathlib.Path, а таблицы читают модулем csv.

Работа с JSON в Python не требует сторонних библиотек: модуль `json` входит в стандартную библиотеку и превращает словари и списки в текст и обратно. Рядом с ним почти всегда нужны ещё три вещи: функция `open` с конструкцией `with`, модуль `pathlib` для путей и модуль `csv` для таблиц. Ниже — всё по порядку, с рабочим кодом и граблями, на которых спотыкаются чаще всего.

## Функция open в Python: режимы и кодировка

Любой текстовый файл открывается встроенной функцией `open(path, mode, encoding=...)`. Она возвращает файловый объект, у которого есть методы `read`, `readline`, `write` и который можно перебирать в цикле построчно.

| Режим | Что делает |
| --- | --- |

| `"r"` | Чтение (по умолчанию). Файла нет — `FileNotFoundError` |

| `"w"` | Запись. Файл создаётся или **обнуляется сразу при открытии** |

| `"a"` | Дописать в конец, существующее содержимое не трогается |

| `"x"` | Создать новый файл; если он уже есть — `FileExistsError` |

| `"b"` | Добавка к режиму: работа с байтами (`"rb"`, `"wb"`) |

### Зачем нужен with

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

```
with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("Первая строка\n")
    f.write("Вторая строка\n")

with open("notes.txt", encoding="utf-8") as f:
    for line in f:
        print(line.rstrip("\n"))
```

Перебор файла в цикле читает его по одной строке и не загружает целиком в память, поэтому так можно обрабатывать и гигабайтные логи. Метод `f.read()` вернёт всё содержимое одной строкой, `f.readlines()` — список строк.

### Как записать строку в файл и не стереть старое

Самая частая ошибка новичка — открыть файл в режиме `"w"`, чтобы «дописать» строчку. Режим `"w"` очищает файл в момент открытия, ещё до первого `write`. Чтобы добавить запись в конец, используйте `"a"`, а чтобы гарантированно не перезаписать чужой файл — `"x"`.

```
with open("log.txt", "a", encoding="utf-8") as f:
    f.write("новая запись\n")
```

Учтите, что `write` не добавляет перевод строки сам — `"\n"` пишется явно. Можно также использовать `print(..., file=f)`: он поставит перенос за вас.

## Кодировка UTF-8: почему появляются «кракозябры»

До Python 3.15 функция `open()` без параметра `encoding` брала кодировку системы. На Linux и macOS это UTF-8, а на Windows с русской локалью — cp1251. Скрипт, который нормально работал на сервере, на ноутбуке коллеги выводил «Р•РґР°» вместо «Еда» или падал с `UnicodeDecodeError`.

В Python 3.15 по PEP 686 режим UTF-8 включён по умолчанию: `open()` без `encoding` читает и пишет UTF-8 на всех ОС. Вернуть старое поведение можно переменной окружения `PYTHONUTF8=0` или флагом `-X utf8=0`, а явно попросить системную кодировку — `encoding="locale"`.

```
import sys
print(sys.flags.utf8_mode)   # 1 в Python 3.15

open("legacy.txt", encoding="locale")   # системная кодировка
open("old_excel.csv", encoding="cp1251")  # файл, сохранённый в cp1251
```

Обратная сторона: старый CSV из Excel или скрипта на Windows, сохранённый в cp1251, теперь будет прочитан как UTF-8 и вызовет `UnicodeDecodeError`. Лечится так: прочитать с `encoding="cp1251"` и пересохранить в UTF-8. Хороший тон не меняется — указывайте `encoding` явно, тогда код одинаково ведёт себя на любой версии.

## pathlib в Python: пути как объекты

Склеивать пути строками (`"data" + "/" + name`) неудобно и ломается на разных ОС. Модуль `pathlib` даёт класс `Path`, где пути соединяются оператором `/`, а у объекта есть понятные свойства.

```
from pathlib import Path

data = Path("data")
csv_path = data / "expenses.csv"

print(csv_path.name)    # expenses.csv
print(csv_path.stem)    # expenses
print(csv_path.suffix)  # .csv
print(csv_path.parent)  # data
print(csv_path.exists())

data.mkdir(parents=True, exist_ok=True)
(data / "hello.txt").write_text("Привет", encoding="utf-8")
print((data / "hello.txt").read_text(encoding="utf-8"))

for p in data.glob("*.csv"):
    print(p)
```

Методы `read_text` и `write_text` заменяют связку `open` + `with` для небольших файлов. Объект `Path` можно передать и в обычный `open`, и в `json`, и в `csv` — все они его принимают. Путь относительно самого скрипта, а не текущей папки, получают так: `Path(__file__).parent / "data"`.

В Python 3.14 у `Path` появились методы `copy()` и `move()`, так что для простого копирования больше не нужен `shutil`. В той же версии в стандартную библиотеку добавили модуль `compression.zstd` для сжатия Zstandard (PEP 784).

## JSON в Python: load, dump, loads, dumps

JSON — текстовый формат обмена данными: его отдают API, в нём хранят конфиги и отчёты. Модуль `json` умеет две операции: сериализацию (Python-объект → JSON-текст) и десериализацию (текст → объект). Методов четыре, и путаница между ними — частый вопрос.

| Функция | Что делает |
| --- | --- |

| `json.dumps(obj)` | Объект → строка JSON |

| `json.loads(s)` | Строка JSON → объект |

| `json.dump(obj, f)` | Объект → записать в открытый файл |

| `json.load(f)` | Прочитать JSON из открытого файла |

Запомнить просто: буква `s` на конце означает string — работа со строкой; без неё — с файлом.

### Соответствие типов

| Python | JSON |
| --- | --- |

| `dict` | объект `{}` |

| `list`, `tuple` | массив `[]` |

| `str` | строка |

| `int`, `float` | число |

| `True` / `False` | `true` / `false` |

| `None` | `null` |

Кортеж при обратном чтении станет списком, а множества, даты и `Decimal` модуль не знает и выдаст `TypeError: Object of type ... is not JSON serializable`.

### Как сохранить JSON в файл

```
import json
from pathlib import Path

report = {
    "month": "2026-10",
    "total": 15230.5,
    "categories": {"еда": 8200, "транспорт": 2100},
    "tags": ["дом", "работа"],
}

path = Path("data/report.json")
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
    json.dump(report, f, ensure_ascii=False, indent=2)
```

Параметр `ensure_ascii=False` сохраняет кириллицу как есть. Без него вместо «еда» в файле окажется `"еда"` — это корректный JSON, но читать его человеку невозможно. `indent=2` делает красивое форматирование с отступами.

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

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

print(data["total"])               # 15230.5
print(data["categories"]["еда"])   # 8200
```

Результат — обычный словарь или список, с ним работают привычными средствами. Если файл не JSON или оборван, будет исключение `json.JSONDecodeError` с номером строки и позиции — его стоит ловить, когда данные приходят извне.

### Парсинг JSON-строки и красивый вывод

```
import json

raw = '{"name": "Анна", "skills": ["python", "sql"], "active": true}'
user = json.loads(raw)
print(user["skills"][0])   # python

print(json.dumps(user, ensure_ascii=False, indent=2, sort_keys=True))
```

`sort_keys=True` сортирует ключи при выводе — удобно, чтобы сравнивать файлы в git. Для быстрого форматирования файла из терминала есть встроенная утилита: `python -m json.tool data/report.json`.

Ответ HTTP API — тоже строка JSON. Библиотека `requests` разбирает её методом `response.json()`, а внутри это тот же `json.loads`.

### Подвох: ключи JSON всегда строки

```
d = {1: "январь", 2: "февраль"}
back = json.loads(json.dumps(d))
print(back == d)   # False
print(back)        # {'1': 'январь', '2': 'февраль'}
# back[1] -> KeyError, нужно back["1"]

fixed = {int(k): v for k, v in back.items()}
```

В JSON ключи объекта могут быть только строками, поэтому числовые ключи после круга сериализации превращаются в строки. Приводите их обратно при чтении.

### Свои типы: даты и Decimal

```
import json
from datetime import date

def default(obj):
    if isinstance(obj, date):
        return obj.isoformat()
    raise TypeError(f"не умею сериализовать {type(obj).__name__}")

print(json.dumps({"day": date(2026, 10, 1)}, default=default))
# {"day": "2026-10-01"}
```

Функция в параметре `default` вызывается для каждого неизвестного объекта. Для сложных случаев можно унаследоваться от `json.JSONEncoder` и переопределить метод `default`.

### Как дописать элемент в JSON-массив

JSON нельзя дописать в конец, как текстовый лог: файл нужно прочитать, изменить и записать целиком.

```
path = Path("data/items.json")
items = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
items.append({"id": len(items) + 1, "title": "новый"})
path.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")
```

Если записей много и они добавляются постоянно, удобнее формат JSON Lines: по одному JSON-объекту на строку, файл открывается в режиме `"a"`.

## CSV в Python: модуль csv

CSV — таблица в текстовом файле: строки — записи, значения разделены запятыми. Модуль `csv` правильно обрабатывает кавычки, запятые внутри значений и переносы, чего не сделает ручной `split(",")`.

```
import csv

with open("data/expenses.csv", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    total = 0.0
    for row in reader:
        total += float(row["amount"])
        print(row["date"], row["category"], row["amount"])
print("Итого:", total)
```

`csv.DictReader` превращает каждую строку в словарь по заголовку файла. Все значения — строки: числа и даты приводите сами (`float`, `int`, `date.fromisoformat`). Файл с разделителем «;» (так сохраняет русский Excel) читают с параметром `delimiter=";"`.

```
rows = [
    {"date": "2026-10-01", "category": "еда", "amount": 450},
    {"date": "2026-10-02", "category": "транспорт", "amount": 60},
]
with open("data/out.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["date", "category", "amount"])
    writer.writeheader()
    writer.writerows(rows)
```

При записи обязательно `newline=""`: без него на Windows между строками появятся пустые. Если файл будут открывать в Excel, используйте `encoding="utf-8-sig"` — метка BOM помогает Excel распознать UTF-8.

## От CSV к JSON: полный пример

```
import csv
import json
from collections import defaultdict
from pathlib import Path

src = Path("data/expenses.csv")
dst = Path("data/report.json")

by_category = defaultdict(float)
with src.open(newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        by_category[row["category"]] += float(row["amount"])

report = {"total": sum(by_category.values()), "categories": dict(by_category)}
dst.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(dst.read_text(encoding="utf-8"))
```

Это ядро любого скрипта-отчёта: вход в CSV, агрегация в словаре, выход в JSON.

## Типичные ошибки

- `FileNotFoundError` — путь относительный и считается от текущей папки запуска, а не от скрипта. Используйте `Path(__file__).parent`.
- Файл обнулился — открыли в `"w"` вместо `"a"`.
- `UnicodeDecodeError` — файл в другой кодировке; укажите её явно (`cp1251`, `utf-8-sig`).
- `TypeError: Object of type set is not JSON serializable` — преобразуйте в список или передайте `default`.
- `KeyError` после загрузки JSON — числовые ключи стали строками.
- Свой файл назван `json.py` или `csv.py` — он перекрывает стандартный модуль, и `import json` импортирует его.

## Итог

Для JSON в Python достаточно стандартной библиотеки: `json.load`/`json.dump` для файлов, `loads`/`dumps` для строк, `ensure_ascii=False` и `indent` для читаемого вывода. Файлы открывайте через `with` с явной кодировкой, пути стройте через `pathlib`, таблицы читайте `csv.DictReader`. В курсе по Python эта тема разобрана на практике: трекер расходов читает траты из CSV и сохраняет отчёт в JSON.

## FAQ

**Чем json.load отличается от json.loads?**

json.load читает JSON из открытого файлового объекта, а json.loads разбирает строку. Аналогично json.dump пишет в файл, а json.dumps возвращает строку.

**Как сохранить русские буквы в JSON без \u-последовательностей?**

Передайте ensure_ascii=False в json.dump или json.dumps и откройте файл с encoding="utf-8".

**Нужно ли ставить библиотеку для работы с JSON в Python?**

Нет, модуль json встроен в стандартную библиотеку, достаточно написать import json.

**Можно ли хранить в JSON дату или множество?**

Напрямую нет: их нужно преобразовать, например дату в строку ISO через isoformat, а множество в список, либо передать функцию в параметр default.

**Как прочитать большой JSON, который не помещается в память?**

Стандартный json.load читает файл целиком; для больших объёмов храните данные в формате JSON Lines и разбирайте построчно через json.loads.


---
Source: Skillaria.Top (https://skillaria.top)
