💻 Примеры JSON и datetime

⚡ json и datetime на одном экране

import json
from datetime import datetime

event = {"title": "Deploy", "when": datetime(2025, 6, 15, 9, 30)}
event["when"] = event["when"].isoformat()      # datetime сам JSON не умеет
text = json.dumps(event, ensure_ascii=False)   # {"title": "Deploy", "when": "2025-06-15T09:30:00"}
back = json.loads(text)
back["when"] = datetime.fromisoformat(back["when"])
ФункцияНаправлениеЛовушка
json.dumps / dumpPython → строка / файлне сериализует set, datetime
json.loads / loadстрока / файл → Pythonкортеж вернётся списком
strftime(fmt)дата → строкаформат нужно помнить руками
strptime(s, fmt)строка → датаfmt должен совпасть со строкой
Топ-3 ошибки: перепутать dumps/dump · одинарные кавычки в JSON-строке · сериализовать datetime напрямую вместо .isoformat().

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

Пример 1. dumps/loads и соответствие типов

import json

data = {
    "name": "Alice",
    "age": 25,
    "is_student": False,
    "score": None,
    "tags": ("python", "beginner"),
}
text = json.dumps(data, ensure_ascii=False)
print(text)
# {"name": "Alice", "age": 25, "is_student": false, "score": null, "tags": ["python", "beginner"]}

restored = json.loads(text)
print(type(data["tags"]), type(restored["tags"]))
# <class 'tuple'> <class 'list'>
print(restored)
# {'name': 'Alice', 'age': 25, 'is_student': False, 'score': None, 'tags': ['python', 'beginner']}

Что происходит: False/None становятся false/null — так того требует формат JSON, у него нет своих True/False с большой буквы. Кортеж tags сериализуется как массив и после loads возвращается уже списком — JSON не различает кортеж и список, оба превращаются в array.

Пример 2. Форматирование: indent, ensure_ascii, sort_keys

payload = {"b": 2, "a": 1, "city": "Москва"}

print(json.dumps(payload))
# {"b": 2, "a": 1, "city": "\u041c\u043e\u0441\u043a\u0432\u0430"}

print(json.dumps(payload, sort_keys=True))
# {"a": 1, "b": 2, "city": "\u041c\u043e\u0441\u043a\u0432\u0430"}

print(json.dumps(payload, ensure_ascii=False))
# {"b": 2, "a": 1, "city": "Москва"}

print(json.dumps(payload, indent=2, sort_keys=True))
# {
#   "a": 1,
#   "b": 2,
#   "city": "\u041c\u043e\u0441\u043a\u0432\u0430"
# }

Что происходит: по умолчанию не-ASCII символы экранируются как \uXXXX — валидный JSON, но нечитаемый человеком; ensure_ascii=False печатает кириллицу как есть. sort_keys и ensure_ascii независимы друг от друга — их можно комбинировать с indent для читаемых конфигов.

Пример 3. Запись и чтение файла

from pathlib import Path

weather = {"city": "Berlin", "temperature": 22.5}
path = Path("weather.json")
path.write_text(json.dumps(weather, ensure_ascii=False, indent=2), encoding="utf-8")
print(path.read_text(encoding="utf-8"))
# {
#   "city": "Berlin",
#   "temperature": 22.5
# }

with path.open(encoding="utf-8") as f:
    loaded = json.load(f)
print(loaded)          # {'city': 'Berlin', 'temperature': 22.5}

path.unlink()
print(path.exists())   # False

Что происходит: json.dump/json.load (без s) работают с открытым файлом напрямую, а dumps/loads — со строкой. Здесь для записи выбран write_text с явным encoding="utf-8", чтобы не зависеть от кодировки системы; тот же файл открыт для чтения через open(..., encoding="utf-8"), как того требует json.load.

Пример 4. JSONDecodeError на битом JSON

bad_variants = [
    '{"name": "Alice", "age": 25,}',   # лишняя запятая перед }
    "{'name': 'Alice'}",               # одинарные кавычки
]
for variant in bad_variants:
    try:
        json.loads(variant)
    except json.JSONDecodeError as error:
        print(f"Ошибка: {error}")
# Ошибка: Expecting property name enclosed in double quotes: line 1 column 29 (char 28)
# Ошибка: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

Что происходит: оба варианта ловит одна и та же ошибка — парсер JSON требует ключи строго в двойных кавычках и не прощает висячую запятую перед закрывающей скобкой (в отличие от Python-словарей, где висячая запятая допустима). Текст сообщения указывает позицию (line/column/char) — полезно при разборе внешнего файла.

Пример 5. Что JSON не умеет сериализовать: set

try:
    json.dumps({"values": {1, 2, 3}})
except TypeError as error:
    print(f"TypeError: {error}")
# TypeError: Object of type set is not JSON serializable

print(json.dumps({"values": list({1, 2, 3})}))          # {"values": [1, 2, 3]}
print(json.dumps({"values": {1, 2, 3}}, default=list))  # {"values": [1, 2, 3]}

Что происходит: в JSON есть только object и array, множества там нет — json.dumps не знает, как его записать. Явное list(...) — самый понятный способ обойти это; параметр default=list делает то же автоматически для всех несериализуемых объектов, которые умеют превращаться в список (это пригодится и в примере 11 для дат).

⚠️ Проверить по документации: параметр default выходит за рамки лекции — см. json.dump(..., default=...).

Пример 6. Вложенные структуры: конфиг с массивом объектов

config = {
    "app": "shop",
    "servers": [
        {"host": "api-1", "port": 8000},
        {"host": "api-2", "port": 8001},
    ],
}
text = json.dumps(config, indent=2)
print(text)
# {
#   "app": "shop",
#   "servers": [
#     {
#       "host": "api-1",
#       "port": 8000
#     },
#     {
#       "host": "api-2",
#       "port": 8001
#     }
#   ]
# }

parsed = json.loads(text)
print(parsed["servers"][1]["host"])   # api-2

Что происходит: JSON вкладывается произвольно глубоко — здесь список словарей внутри словаря, типичная форма конфигурационного файла или ответа API. После loads добраться до значения можно обычной цепочкой индексов, как и в любой вложенной структуре Python (см. урок 36).

Пример 7. Создание даты и strftime

from datetime import datetime

launch = datetime(2025, 6, 15, 9, 30)
print(launch)                                   # 2025-06-15 09:30:00
print(launch.strftime("%d.%m.%Y %H:%M"))        # 15.06.2025 09:30
print(launch.strftime("%A, %d %B %Y"))          # Sunday, 15 June 2025
print(launch.weekday())                         # 6 — воскресенье (0 = понедельник)

Что происходит: datetime(year, month, day, hour, minute) собирает объект даты из чисел; strftime превращает его в строку по коду формата — %A/%B дают название дня недели и месяца на английском (локаль по умолчанию). weekday() возвращает номер дня недели, где понедельник — 0, поэтому воскресенье — 6.

Пример 8. strptime: строка → дата, и что будет при несовпадении формата

deadline = datetime.strptime("01.12.2025", "%d.%m.%Y")
print(deadline)   # 2025-12-01 00:00:00

try:
    datetime.strptime("15-06-2025", "%d/%m/%Y")
except ValueError as error:
    print(f"ValueError: {error}")
# ValueError: time data '15-06-2025' does not match format '%d/%m/%Y'

Что происходит: формат во втором аргументе должен буквально совпадать со строкой, включая разделители — в строке дефисы, в формате слэши, и strptime честно об этом сообщает вместо того, чтобы угадывать. Время не указано, поэтому в deadline оно заполняется нулями.

Пример 9. timedelta: арифметика и сравнение дат

from datetime import date, timedelta

start = datetime(2025, 6, 15)
end = start + timedelta(days=14)
print(end.strftime("%Y-%m-%d"))   # 2025-06-29

difference = deadline - launch
print(difference)        # 168 days, 14:30:00
print(difference.days)   # 168

today = date(2025, 6, 20)
print(today > start.date())   # True

Что происходит: timedelta можно и прибавлять к дате, и получать вычитанием двух дат — разница между deadline (1 декабря, полночь) и launch (15 июня, 9:30) выходит не круглым числом дней, а «168 дней и 14:30». date (без времени) и datetime сравниваются раздельно — отсюда start.date(), чтобы сравнить today с однотипным объектом.

Пример 10. Наивные и осведомлённые о часовом поясе даты

from datetime import timezone

naive = datetime(2025, 6, 15, 12, 0)
aware = datetime(2025, 6, 15, 12, 0, tzinfo=timezone.utc)
print(naive.tzinfo)   # None
print(aware.tzinfo)   # UTC

try:
    print(naive < aware)
except TypeError as error:
    print(f"TypeError: {error}")
# TypeError: can't compare offset-naive and offset-aware datetimes

Что происходит: datetime без tzinfo называется «наивным» — Python не знает, в каком часовом поясе эти 12:00. Смешивать наивные и «осведомлённые» (aware) объекты в сравнении нельзя — это частый источник багов при работе с датами из разных источников (сервер в UTC, пользователь в своём поясе).

⚠️ Проверить по документации: часовые пояса выходят за рамки лекции — см. datetime.timezone и для сложных случаев (переход на летнее время, региональные правила) — сторонний пакет zoneinfo.

Пример 11. Совмещаем темы: datetime внутри JSON

Ради этого сочетания урок и объединяет json и datetime: формат JSON умеет строки, числа, булевы значения — но не объекты datetime.

event = {"title": "Deploy", "when": datetime(2025, 6, 15, 9, 30)}
try:
    json.dumps(event)
except TypeError as error:
    print(f"TypeError: {error}")
# TypeError: Object of type datetime is not JSON serializable

serializable = {**event, "when": event["when"].isoformat()}
text = json.dumps(serializable, ensure_ascii=False)
print(text)   # {"title": "Deploy", "when": "2025-06-15T09:30:00"}

restored = json.loads(text)
restored["when"] = datetime.fromisoformat(restored["when"])
print(restored)             # {'title': 'Deploy', 'when': datetime.datetime(2025, 6, 15, 9, 30)}
print(type(restored["when"]))   # <class 'datetime.datetime'>

Что происходит: прямая сериализация datetime падает так же, как падала сериализация set в примере 5 — оба не входят в базовый набор типов JSON. Рабочий приём — вручную превратить дату в строку через .isoformat() перед dumps и вручную же разобрать её обратно через datetime.fromisoformat() после loads; сам JSON про даты ничего не знает и не преобразует их автоматически.

Что дальше

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