Прямой ответ
Выбор сериализации определяется двумя вопросами: кто читает данные и доверяете ли вы источнику. Для обмена с внешним миром (API, конфиги, логи) — JSON: стандартный json достаточен почти везде, orjson — когда профиль показал, что сериализация стала узким местом (в разы быстрее, native-поддержка datetime/UUID/dataclasses). pickle — только для внутреннего кеша/состояния, которое читает тот же доверенный код той же версии: десериализация pickle из недоверенного источника = выполнение произвольного кода, это не преувеличение, а документированное свойство. Для больших объёмов аналитики — колоночные форматы (parquet), для компактной передачи — msgpack.
Стандартный json: крючки, о которых забывают
json.dumps/loads покрывает базовые типы; за их пределами — два официальных крючка: default= (функция «как сериализовать неизвестный объект» — default=str как грубый вариант, лучше — явная обработка datetime/dataclasses через asdict) и object_hook=/object_pairs_hook= (восстановление объектов на десериализации). Практические детали, которые ловят на проде: ключи словаря приводятся к строкам ({1: "a"} станет {"1": "a"}) — целочисленные ключи не переживают цикл; ensure_ascii=True по умолчанию экранирует кириллицу (\u041f) — для человекочитаемых файлов ensure_ascii=False; NaN/Infinity — не валидный JSON (стандартный json пишет их вольно, строгие парсеры на той стороне падают); точность float — для денег не подходит (Decimal строкой или целые копейки). Для моделей с валидацией разумнее не ручные хуки, а pydantic-сериализация (model_dump_json) — связка разбирается в статье про pydantic vs dataclasses.
orjson: когда стандартный json тормозит
orjson — Rust-реализация с заметным выигрышем (типично 2–10× на больших payload) и приятными мелочами: datetime/UUID/dataclasses/numpy сериализуются нативно (без default-лапши), вывод — bytes (не str), опции флагами (OPT_SORT_KEYS, OPT_INDENT_2 — только 2 пробела). Ограничения, о которые спотыкаются при миграции: нет object_hook-эквивалента (десериализация — в обычный dict, объекты восстанавливаются сами), сериализация dict с нестроковыми ключами строже, default обязана возвращать JSON-совместимое сразу. Правило перехода: не «заменить везде», а по профилю — на сервисах с тяжёлым JSON-трафиком выигрыш измерим, в скрипте на сто вызовов — нет.
pickle: мощь и красная кнопка
pickle сериализует почти любой Python-объект с состоянием — и именно поэтому опасен: поток pickle — это программа для виртуальной машины, и pickle.loads от злоумышленника выполняет произвольный код (документация предупреждает прямо). Границы допустимого: внутренний кеш/персист состояния в доверенном контуре (и то — с оговоркой ниже), передача между вашими воркерами в защищённой сети. Недопустимо: pickle через сеть от внешних источников, «сохранил объект в файл на общем хранилище», Celery-сериализация pickle в проде (дефолтный JSON выбран не зря). Вторая, недооценённая проблема — хрупкость: pickle привязан к структуре классов; переименовали поле — старый кеш не читается (или, хуже, читается неверно). Для состояния, которое должно пережить изменения кода, — версионируемые форматы данных (JSON/msgpack со схемой), не pickle.
Большие данные и компактность
JSON плох для таблиц: текстовость, повтор ключей, парсинг построчно. Лестница альтернатив: JSONL (строка на запись — потоковая обработка без чтения всего файла, идеален для логов/датасетов); msgpack (двоичный JSON-подобный — компактнее и быстрее, но нечитаем глазами); parquet (колоночный, сжатие, типы, предикат-пушдаун — стандарт аналитики; работа из pandas — в статье про файлы данных); sqlite (когда нужен не формат, а хранилище с запросами). Правило: формат выбирается по читателю — человек (конфиг) → JSON/TOML; другой сервис → JSON/msgpack; аналитика → parquet; состояние приложения → JSON-совместимая схема с версией.
Безопасность и гигиена
Чек-лист границ сериализации: недоверенный вход — только JSON с валидацией (pydantic) или msgpack со схемой, никогда pickle/yaml.load (у PyYAML та же беда — используйте safe_load); секреты не сериализуются в логи/кеши (подходы к секретам — в статье про env и секреты); версии схем — явное поле schema_version в payload, чтобы эволюция форматов не ломала старые данные молча; размер — лимиты на входящий payload (гигабайтный «JSON» в теле запроса — вектор DoS). И remember: «десериализация — граница доверия» — единственная мысль, которую нужно вынести из этой темы.
Практический сценарий: кеш результатов между запусками
Задача: скрипт ежедневно собирает статистику из внешнего API, тяжёлые запросы хочется кешировать между запусками. Первый порыв — pickle.dump(results, open("cache.pkl", "wb")) — работает ровно до первого рефакторинга: переименовали класс DailyStats — кеш не десериализуется (или, что хуже, тихо соберётся криво), а если кеш-файл окажется в общей папке — pickle из недоверенного источника превращается в канал выполнения кода. Рабочее решение — JSONL со схемой: каждая запись — строка {"date": "2026-10-20", "endpoint": "/stats", "payload": {...}, "schema_version": 1}; чтение — построчно с валидацией pydantic-моделью, запись — дописыванием (потоковость: прерванный скрипт не портит уже сохранённое). Эволюция схемы явная: версия в каждой строке, миграция — функция-обновление старых записей при чтении, а не «удалим кеш и соберём заново» (иногда допустимо, но это решение, а не побочный эффект). Объём критичен — строки сжимаются gzip на лету (gzip.open(..., "wt", encoding="utf-8")) — текстовый кеш сжимается отлично. Итог: кеш переживает рефакторинг кода, читается глазами при отладке, безопасно хранится где угодно — три свойства, которых у pickle-версии не было ни одного.
FAQ
Насколько orjson быстрее на практике?
На сериализации крупных структур — разы (Rust, SIMD); на маленьких dict разница в микросекундах и не стоит зависимости. Меряйте на своём payload: timeit на репрезентативном объекте.
Чем JSONL лучше JSON для логов? Потоковость: строки дописываются и читаются независимо, файл не «ломается» целиком при обрыве записи, обработка — построчно без загрузки гигабайтов. Для журналов и датасетов это де-факто стандарт.
Можно ли pickle для кеша результатов между запусками? Технически да, если кеш локальный, приватный и вы принимаете риск несовместимости версий. Практичнее — joblib (обёртка с компрессией) или JSON/msgpack со схемой: переживают рефакторинг классов.
Что использовать для конфигов: JSON или TOML? TOML для человекоредактируемых конфигов (комментарии, читаемость, стандарт в pyproject); JSON — для машинного обмена. YAML популярен, но коварен синтаксисом (неявные типы — «Norway problem»); с ним — только safe_load.
Источники: документация json и pickle (docs.python.org), orjson (github.com/ijl/orjson) — ссылки в шапке.
FAQ
Как сериализовать datetime в JSON правильно?
ISO 8601 с явной зоной: dt.isoformat() для aware-объектов, парсинг — datetime.fromisoformat. Наивные datetime сериализовать нельзя без договорённости о зоне — сначала осознанный tzinfo, потом строка; orjson делает ISO-формат автоматически.
Что использовать для кеша в Redis: JSON или pickle? JSON: кеш читают разные сервисы и версии кода, а pickle привязан к классам и опасен при компрометации хранилища. Компактность JSON компенсируется сжатием на клиенте; pickle в общем кеше — ложная экономия.